手寫Struts,帶你深入原始碼中心解析
個人剖析,不喜勿噴
掃碼關注公眾號,不定期更新干活
在此申明本博文並非原創,原文:http://blog.csdn.net/lenotang/article/details/3336623,本文章是在此文章基礎上進行優化。也談不上優化,只是加上了點自己的想法
jar包準備
為什麼會用到這兩個jar包呢,因為我需要通過這個jar來解析xml配置檔案。
新建專案
流程梳理
- struts配置檔案
```
<?xml version="1.0" encoding="UTF-8"?>
```
- 熟悉struts的朋友都清楚struts.xml配置檔案的重要性,這個配置檔名字是可以更改的,這裡簡單解釋下這個配置檔案的作用,首先我們找到action這個節點這個action的name是login,就是說前臺中請求這個login經過這個配置檔案解析就會把這個請求交給action中的class屬性,也就是上面的
```
org.zxh.action.LoginAction
```
具體的是交由這個類的login(method)這個方法。這個方法會方法一個string型別的字串,如果返回的是success就將頁面重定向到index.jsp如果是login就重定向到login.jsp。這個配置檔案就是這樣的作用。因為是自己寫的,所以這裡並不會想struts框架那樣封裝了很多東西,這裡只是為了讓讀者更加深入的理解struts的執行機制。
如何將我們寫的struts.xml檔案在程式中啟動呢?
- 剛入門的同志可能會疑問,寫一個配置檔案就能處理前後臺互動了?答案當然是不能。這裡給大家普及一下web基礎接觸filter的,每次互動需要filter(jsp就是特殊的servlet),所以想實現互動我們就得新建一個servlet,在這個servlet裡我們去讀我們寫的struts.xml檔案,通過讀到的資訊決定下一步的操作。那麼如何啟動一個filter呢?這個不多說,直接在web專案中的web.xml配置攔截器就會執行filter。
新建filter(FilterDispatcher)
- 這個servlet就是struts的核心過濾器,需要先繼承過濾器。
```
public class FilterDispatcher implements Filter{
@Override
public void destroy() {
// TODO Auto-generated method stub
}
@Override
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2) throws IOException, ServletException {
// TODO Auto-generated method stub
}
@Override
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub
}
}
```
- Filter中我們要在初始化函式(init)中對一些引數進行初始化,對那些資料初始化呢,對!當然是拿配置檔案的資訊啦。配置檔案是.xml這裡我用dom4j讀取.xml配置檔案。 把struts.xml配置檔案放在src下,(可以放在其他地方,這裡的地址填的對應就行了)
// 獲得xml配置檔案
String webRootPath = getClass().getClassLoader()
.getResource("struts.xml").getPath();
- 拿到配置檔案路徑之後開始讀取,這裡我講讀到的資料封裝到一個map裡面。在封裝在Map中我們仔細觀察一下配置檔案
- 其實我們放在Map裡面就是這四個屬性的值,有了這四個值我們就可以完成一次前後臺互動的映射了。所以為了方便這裡封裝成javabean。
package org.zxh.util;
import java.util.HashMap;
import java.util.Map;
/**
* 將action屬性封裝成類
* @author 87077
*
*/
public class ActionConfig {
//action 給別人呼叫的名字
private String name;
//action對應程式中的action類
private String clazzName;
//action中的方法
private String method;
//返回結果不知一條 所以用Map
private Map<String, String> resultMap = new HashMap<String, String>();
public ActionConfig(){
}
public ActionConfig(String name , String clazzName , String method , Map<String, String> resultMap){
this.name=name;
this.clazzName=clazzName;
this.method=method;
this.resultMap=resultMap;
}
public String getName() {
return name;
}
public String getClazzName() {
return clazzName;
}
public String getMethod() {
return method;
}
public Map<String, String> getResultMap() {
return resultMap;
}
public void setName(String name) {
this.name = name;
}
public void setClazzName(String clazzName) {
this.clazzName = clazzName;
}
public void setMethod(String method) {
this.method = method;
}
public void setResultMap(Map<String, String> resultMap) {
this.resultMap = resultMap;
}
}
- 有了javabean 我們開始解析xml檔案
package org.zxh.util;
import java.io.File;
import java.util.List;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
/**
* 採用dom4j解析xml配置檔案
*
* @author 87077
*
*/
public class ConfigUtil {
/**
* @param fileName
* 待解析的檔案
* @param map
* 存放解析的資料
*/
public static void parseConfigFile(String fileName,
Map<String, ActionConfig> map) {
SAXReader reader = new SAXReader();
try {
Document doc = reader.read(new File("D:\\zxh\\soft\\apache-tomcat-7.0.70\\apache-tomcat-7.0.70\\webapps\\MyStruts\\WEB-INF\\classes\\struts.xml"));
Element root = doc.getRootElement();
List<Element> list = root.selectNodes("package/action");
for (Element element : list) {
// 封裝成ActionConfig物件,儲存在map中
ActionConfig config = new ActionConfig();
// 獲取action中的值
String name = element.attributeValue("name");
String clazzName = element.attributeValue("class");
String method = element.attributeValue("method");
// 將值傳入javabean中
config.setName(name);
config.setClazzName(clazzName);
// 如果沒有設定執行method 執行預設的
if (method == null || "".equals(method)) {
method = "execute";
}
config.setMethod(method);
// 繼續向下獲取action中的返回方法
List<Element> resultList = element.selectNodes("result");
for (Element resultElement : resultList) {
String resultName = resultElement.attributeValue("name");
String urlPath = resultElement.getTextTrim();
if (resultName == null || "".equals(resultName)) {
resultName = "success";
}
config.getResultMap().put(resultName, urlPath);
}
map.put(name, config);
}
} catch (DocumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
- 現在我們在回到過濾器上,上面兩個類就是為了解析xml的。所以在Filter中的init方法裡我們就可以將解析的資料放到我們的全域性Map中
@Override
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub 過濾器的初始化過程
// 獲得xml配置檔案
String webRootPath = getClass().getClassLoader()
.getResource("struts.xml").getPath();
// 將xml配置檔案解析裝在到map中
ConfigUtil.parseConfigFile(webRootPath, map);
}
過濾器的執行
- 過濾器真正執行是在doFilter方法開始時。
public void doFilter(ServletRequest arg0, ServletResponse arg1,
FilterChain arg2)
doFilter()方法類似於Servlet介面的service()方法。當客戶端請求目標資源的時候,容器就會呼叫與這個目標資源相關聯的過濾器的 doFilter()方法。其中引數 request, response 為 web 容器或 Filter 鏈的上一個 Filter 傳遞過來的請求和相應物件;引數 chain 為代表當前 Filter 鏈的物件,在特定的操作完成後,可以呼叫 FilterChain 物件的 chain.doFilter(request,response)方法將請求交付給 Filter 鏈中的下一個 Filter 或者目標 Servlet 程式去處理,也可以直接向客戶端返回響應資訊,或者利用RequestDispatcher的forward()和include()方法,以及 HttpServletResponse的sendRedirect()方法將請求轉向到其他資源。這個方法的請求和響應引數的型別是 ServletRequest和ServletResponse,也就是說,過濾器的使用並不依賴於具體的協議。
- 獲取請求域和響應域還有Filter鏈,並設定編碼防止亂碼
//針對http請求,將請求和響應的型別還原為HTTP型別
HttpServletRequest request = (HttpServletRequest) arg0;
HttpServletResponse response = (HttpServletResponse) arg1;
//設定請求和響應的編碼問題
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
- 獲取請求地址
//獲取請求路徑
String url = request.getServletPath();
- 通過請求去判斷知否攔截過濾這個地址的請求,本文預設過濾所有以.action結尾的請求
//請求地址過濾,如果不是以.action結尾的
if(!url.endsWith(".action")){
//不是.action的放行
arg2.doFilter(request, response);
return ;
}
- 看我之前將xml檔案中資料放入到Map的格式可以看出我是講整個javabean放入Map中名字是action的name。所以下面我就要去那個name(就是請求中的login)
//解析request路徑
int start = url.indexOf("/");
int end = url.lastIndexOf(".");
String path=url.substring(start+1,end);
//通過path去匹配到對應的ActionConfig類。在這裡已經解析到了所有的action的資訊
ActionConfig config = map.get(path);
//匹配不成功就返回找不到頁面錯誤資訊
if(config==null){
response.setStatus(response.SC_NOT_FOUND);
return ;
}
- 獲取了ActionConfig類了,action的所有資訊都儲存在這個javabean類中了,下面的事情就好辦了。下面的只是會用到反射的知識。我們拿到真正action類的名稱後就需要根據名字獲取到這個action的實體類。
//通過ActionConfig獲取完成的類名字
String clazzName=config.getClazzName();
//例項化Action物件,不存在的話就提示錯誤資訊
Object action = getAction(clazzName);
if(action==null){
//說明這個action是錯誤的,在配置檔案中沒有佔到對應的action類
response.setStatus(response.SC_NOT_FOUND);
return ;
}
request引數獲取並賦值給action
- 執行action的方法前很定需要先將request中的引數獲取到,進行賦值,這部才是真正的意義上的互動。
public static void requestToAction(HttpServletRequest request , Object action )
- 將傳進來的action物件進行class話並獲取action實體下的屬性
Class<? extends Object> clazzAction = action.getClass();
//獲取aciton中所有屬性,從前臺獲取的值很多,只有action屬性中有的才會進行反射賦值
Field[] fields = action.getClass().getDeclaredFields();
- 拿到request傳過來的值並進行遍歷
//獲取請求中的名字屬性值
Enumeration<String> names=request.getParameterNames();
String name=names.nextElement();
boolean flag=false;
//需要判斷action屬性中沒有的而請求中有的我們不需要進行反射處理
for (Field field : fields) {
if(name.equals(field.getName())){
flag=true;
}
}
if(!flag){
return;
}
String[] value=request.getParameterValues(name);
- 通過request中的name並且在action中有這個屬性之後我們就需要獲取action這個欄位的屬性。
Class<Object> fieldType=(Class<Object>) clazzAction.getDeclaredField(name).getType();
- 獲取action的改name欄位屬性的set方法
//通過反射呼叫該屬性的set方法
String setName="set"+name.substring(0,1).toUpperCase()+name.substring(1);
Method method=clazzAction.getMethod(setName, new Class[]{fieldType});
- 下面我們就需要將獲取的value按型別
private static Object[] transfer(Class<Object> fieldType , String[] value){
Object[] os = null;
//fieldType 是[]這種型別的,需要將[]去掉
String type=fieldType.getSimpleName().replace("[]", "");
if("String".equals(type)){
os=value;
}else if("int".equals(type)||"Integer".equals(type)){
os = new Integer[value.length];
for (int i = 0; i < os.length; i++) {
os[i] = Integer.parseInt(value[i]);
}
}else if("float".equals(type)||"Float".equals(type)){
os=new Float[value.length];
for (int i = 0; i < os.length; i++) {
os[i]=Float.parseFloat(value[i]);
}
}else if("double".equals(type)||"Double".equals(type)){
os=new Double[value.length];
for (int i = 0; i < os.length; i++) {
os[i]=Double.parseDouble(value[i]);
}
}
return os;
}
- 獲取object資料之後就是講這個object資料通過反射付給action對應的屬性
//判斷是否是陣列屬性
if(fieldType.isArray()){
method.invoke(action, new Object[]{object});
}else {
method.invoke(action, new Object[]{object[0]});
}
這說一下 method.invoke是將action類中method方法這個方法需要的引數就是object<a href="http://www.oschina.net/code/snippet_216465_36771”>詳解
- 有了這個方法我們在回到Filter就可以了
//前置攔截,獲取request裡面的引數,呼叫action的set方法給屬性設定值
BeanUtil.requestToAction(request, action);
- 屬性賦值完成就開始執行action中的method了
private String executeAction(ActionConfig config, Object action) {
String method = config.getMethod();
String result = null;
try {
Method callMethod = action.getClass().getMethod(method,String.class);
result = (String) callMethod.invoke(action, new Object[] {});
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return config.getResultMap().get(result);
}
- 到這裡你已經獲取了配置檔案中前臺對映後應該的result了,那麼就簡單了,直接重定向就可以了,到這裡就實現了struts的前後臺互動。
request.getRequestDispatcher(result).forward(request, response);
驗證正確性
- 下面就在前臺jsp中form表單將資料傳遞給我們的login action看看會不會去執行指定的方法
<form method="post" action="login.action" name="loginForm">
<table width="422" border="1" bgcolor="#0080c0" height="184">
<caption>
<h1>使用者登陸</h1>
</caption>
<tbody>
<tr>
<td> 姓名:</td>
<td> <input type="text" name="username">
</td>
</tr>
<tr>
<td> 密碼:</td>
<td> <input type="password" name="password">
</td>
</tr>
<tr align="center">
<td colspan="2"> <input type="submit" value="登陸"
name="submit"> <input type="reset" value="重置" name="reset">
</td>
</tr>
</tbody>
</table>
</form>
- 效果讀者自行展示吧,到這裡struts的執行機制就講完了,注意知識執行機制裡面還有很多值得我們學習的東西,就好比說這裡有很多過濾器,不同過濾器過濾資料程度不同執行效果不同。希望有機會再和大家分享一些其他關於struts的知識!
上訴原理的原始碼下