第二章 Struts2的工作機制及分析http://www./lzhidj/archive/2014/12/08/213898.html
概述本章講述Struts2的工作原理。 讀者如果曾經(jīng)學(xué)習(xí)過Struts1.x或者有過Struts1.x的開發(fā)經(jīng)驗,那么千萬不要想當然地以為這一章可以跳過。實際上Struts1.x與Struts2并無我們想象的血緣關(guān)系。雖然Struts2的開發(fā)小組極力保留Struts1.x的習(xí)慣,但因為Struts2的核心設(shè)計完全改變,從思想到設(shè)計到工作流程,都有了很大的不同。 Struts2是Struts社區(qū)和WebWork社區(qū)的共同成果,我們甚至可以說,Struts2是WebWork的升級版,他采用的正是WebWork的核心,所以,Struts2并不是一個不成熟的產(chǎn)品,相反,構(gòu)建在WebWork基礎(chǔ)之上的Struts2是一個運行穩(wěn)定、性能優(yōu)異、設(shè)計成熟的WEB框架。 本章主要對Struts的源代碼進行分析,因為Struts2與WebWork的關(guān)系如此密不可分,因此,讀者需要下載xwork的源代碼,訪問http://www./xwork/download.action即可自行下載。 下載的Struts2源代碼文件是一個名叫struts-2.1.0-src.zip的壓縮包,里面的目錄和文件非常多,讀者可以定位到struts-2.1.0-src"struts-2.0.10"src"core"src"main"java目錄下查看Struts2的源文件,如圖14所示。
(圖14) 主要的包和類Struts2框架的正常運行,除了占核心地位的xwork的支持以外,Struts2本身也提供了許多類,這些類被分門別類組織到不同的包中。從源代碼中發(fā)現(xiàn),基本上每一個Struts2類都訪問了WebWork提供的功能,從而也可以看出Struts2與WebWork千絲萬縷的聯(lián)系。但無論如何,Struts2的核心功能比如將請求委托給哪個Action處理都是由xwork完成的,Struts2只是在WebWork的基礎(chǔ)上做了適當?shù)暮喕⒓訌姾头庋b,并少量保留Struts1.x中的習(xí)慣。 以下是對各包的簡要說明:
下表是對一些重要類的說明:
Struts2的工作機制3.1Struts2體系結(jié)構(gòu)圖Strut2的體系結(jié)構(gòu)如圖15所示:
(圖15) 3.2Struts2的工作機制從圖15可以看出,一個請求在Struts2框架中的處理大概分為以下幾個步驟: 1、客戶端初始化一個指向Servlet容器(例如Tomcat)的請求; 2、這個請求經(jīng)過一系列的過濾器(Filter)(這些過濾器中有一個叫做ActionContextCleanUp的可選過濾器,這個過濾器對于Struts2和其他框架的集成很有幫助,例如:SiteMesh Plugin); 3、接著FilterDispatcher被調(diào)用,FilterDispatcher詢問ActionMapper來決定這個請求是否需要調(diào)用某個Action; 4、如果ActionMapper決定需要調(diào)用某個Action,FilterDispatcher把請求的處理交給ActionProxy; 5、ActionProxy通過Configuration Manager詢問框架的配置文件,找到需要調(diào)用的Action類; 6、ActionProxy創(chuàng)建一個ActionInvocation的實例。 7、ActionInvocation實例使用命名模式來調(diào)用,在調(diào)用Action的過程前后,涉及到相關(guān)攔截器(Intercepter)的調(diào)用。 8、一旦Action執(zhí)行完畢,ActionInvocation負責(zé)根據(jù)struts.xml中的配置找到對應(yīng)的返回結(jié)果。返回結(jié)果通常是(但不總是,也可能是另外的一個Action鏈)一個需要被表示的JSP或者FreeMarker的模版。在表示的過程中可以使用Struts2 框架中繼承的標簽。在這個過程中需要涉及到ActionMapper。 注:以上步驟參考至網(wǎng)上,具體網(wǎng)址已忘記。在此表示感謝! 3.3Struts2源代碼分析和Struts1.x不同,Struts2的啟動是通過FilterDispatcher過濾器實現(xiàn)的。下面是該過濾器在web.xml文件中的配置: 代碼清單6:web.xml(截?。?/span> <filter> <filter-name>struts2</filter-name> <filter-class> org.apache.struts2.dispatcher.FilterDispatcher </filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> Struts2建議,在對Struts2的配置尚不熟悉的情況下,將url-pattern配置為/*,這樣該過濾器將截攔所有請求。 實際上,FilterDispatcher除了實現(xiàn)Filter接口以外,還實現(xiàn)了StrutsStatics接口,繼承代碼如下: 代碼清單7:FilterDispatcher結(jié)構(gòu) publicclass FilterDispatcher implements StrutsStatics, Filter { } StrutsStatics并沒有定義業(yè)務(wù)方法,只定義了若干個常量。Struts2對常用的接口進行了重新封裝,比如HttpServletRequest、HttpServletResponse、HttpServletContext等?!∫韵率?/span>StrutsStatics的定義: 代碼清單8:StrutsStatics.java publicinterface StrutsStatics { /** *ConstantfortheHTTPrequestobject. */ publicstaticfinal String HTTP_REQUEST = "com.opensymphony.xwork2.dispatcher.HttpServletRequest"; /** *ConstantfortheHTTPresponseobject. */ publicstaticfinal String HTTP_RESPONSE = "com.opensymphony.xwork2.dispatcher.HttpServletResponse"; /** *ConstantforanHTTPrequest dispatcher}. */ publicstaticfinal String SERVLET_DISPATCHER = "com.opensymphony.xwork2.dispatcher.ServletDispatcher"; /** *Constantfortheservlet context}object. */ publicstaticfinal String SERVLET_CONTEXT = "com.opensymphony.xwork2.dispatcher.ServletContext"; /** *ConstantfortheJSPpage context}. */ publicstaticfinal String PAGE_CONTEXT = "com.opensymphony.xwork2.dispatcher.PageContext"; /**ConstantforthePortletContextobject*/ publicstaticfinal String STRUTS_PORTLET_CONTEXT = "struts.portlet.context"; } 容器啟動后,FilterDispatcher被實例化,調(diào)用init(FilterConfig filterConfig)方法。該方法創(chuàng)建Dispatcher類的對象,并且將FilterDispatcher配置的初始化參數(shù)傳到對象中(詳情請參考代碼清單10),并負責(zé)Action的執(zhí)行。然后得到參數(shù)packages,值得注意的是,還有另外三個固定的包和該參數(shù)進行拼接,分別是org.apache.struts2.static、template、和org.apache.struts2.interceptor.debugging,中間用空格隔開,經(jīng)過解析將包名變成路徑后存儲到一個名叫pathPrefixes的數(shù)組中,這些目錄中的文件會被自動搜尋。 代碼清單9:FilterDispatcher.init()方法 publicvoid init(FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; dispatcher = createDispatcher(filterConfig); dispatcher.init(); String param = filterConfig.getInitParameter("packages"); String packages = "org.apache.struts2.static template org.apache.struts2.interceptor.debugging"; if (param != null) { packages = param + " " + packages; } this.pathPrefixes = parse(packages); } 代碼清單10:FilterDispatcher.createDispatcher()方法 protected Dispatcher createDispatcher(FilterConfig filterConfig) { Map<String,String> params = new HashMap<String,String>(); for (Enumeration e = filterConfig.getInitParameterNames(); e.hasMoreElements(); ) { String name = (String) e.nextElement(); String value = filterConfig.getInitParameter(name); params.put(name, value); } returnnew Dispatcher(filterConfig.getServletContext(), params); } 當用戶向Struts2發(fā)送請求時,FilterDispatcher的doFilter()方法自動調(diào)用,這個方法非常關(guān)鍵。首先,Struts2對請求對象進行重新包裝,此次包裝根據(jù)請求內(nèi)容的類型不同,返回不同的對象,如果為multipart/form-data類型,則返回MultiPartRequestWrapper類型的對象,該對象服務(wù)于文件上傳,否則返回StrutsRequestWrapper類型的對象,MultiPartRequestWrapper是StrutsRequestWrapper的子類,而這兩個類都是HttpServletRequest接口的實現(xiàn)。包裝請求對象如代碼清單11所示: 代碼清單11:FilterDispatcher.prepareDispatcherAndWrapRequest()方法 protectedHttpServletRequest prepareDispatcherAndWrapRequest( HttpServletRequest request, HttpServletResponse response) throws ServletException { Dispatcher du = Dispatcher.getInstance(); if (du == null) { Dispatcher.setInstance(dispatcher); dispatcher.prepare(request, response); } else { dispatcher = du; } try { request = dispatcher.wrapRequest(request, getServletContext()); } catch (IOException e) { String message = "Could not wrap servlet request with MultipartRequestWrapper!"; LOG.error(message, e); thrownew ServletException(message, e); } return request; } request對象重新包裝后,通過ActionMapper的getMapping()方法得到請求的Action,Action的配置信息存儲在ActionMapping對象中,該語句如下:mapping = actionMapper.getMapping(request, dispatcher.getConfigurationManager());。下面是ActionMapping接口的實現(xiàn)類DefaultActionMapper的getMapping()方法的源代碼: 代碼清單12:DefaultActionMapper.getMapping()方法 public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) { ActionMapping mapping = new ActionMapping(); String uri = getUri(request);//得到請求路徑的URI,如:testAtcion.action或testAction!method uri = dropExtension(uri);//刪除擴展名,默認擴展名為action,在代碼中的定義是List extensions = new ArrayList() {{ add("action");}}; if (uri == null) { returnnull; } parseNameAndNamespace(uri, mapping, configManager);//從uri變量中解析出Action的name和namespace handleSpecialParameters(request, mapping);//將請求參數(shù)中的重復(fù)項去掉 //如果Action的name沒有解析出來,直接返回 if (mapping.getName() == null) { returnnull; }
//下面處理形如testAction!method格式的請求路徑 if (allowDynamicMethodCalls) { // handle "name!method" convention. String name = mapping.getName(); int exclamation = name.lastIndexOf("!");//!是Action名稱和方法名的分隔符 if (exclamation != -1) { mapping.setName(name.substring(0, exclamation));//提取左邊為name mapping.setMethod(name.substring(exclamation + 1));//提取右邊的method } } return mapping; } 該代碼的活動圖如下: (圖16) 從代碼中看出,getMapping()方法返回ActionMapping類型的對象,該對象包含三個參數(shù):Action的name、namespace和要調(diào)用的方法method。
如果getMapping()方法返回ActionMapping對象為null,則FilterDispatcher認為用戶請求不是Action,自然另當別論,FilterDispatcher會做一件非常有意思的事:如果請求以/struts開頭,會自動查找在web.xml文件中配置的packages初始化參數(shù),就像下面這樣(注意粗斜體部分): 代碼清單13:web.xml(部分) <filter> <filter-name>struts2</filter-name> <filter-class> org.apache.struts2.dispatcher.FilterDispatcher </filter-class> <init-param> <param-name>packages</param-name> <param-value>com.lizanhong.action</param-value> </init-param> </filter> FilterDispatcher會將com.lizanhong.action包下的文件當作靜態(tài)資源處理,即直接在頁面上顯示文件內(nèi)容,不過會忽略擴展名為class的文件。比如在com.lizanhong.action包下有一個aaa.txt的文本文件,其內(nèi)容為“中華人民共和國”,訪問http://localhost:8081/Struts2Demo/struts/aaa.txt時會有如圖17的輸出:
(圖17) 查找靜態(tài)資源的源代碼如清單14: 代碼清單14:FilterDispatcher.findStaticResource()方法 protectedvoid findStaticResource(String name, HttpServletRequest request, HttpServletResponse response) throws IOException { if (!name.endsWith(".class")) {//忽略class文件 //遍歷packages參數(shù) for (String pathPrefix : pathPrefixes) { InputStream is = findInputStream(name, pathPrefix);//讀取請求文件流 if (is != null) { ……(省略部分代碼) // set the content-type header String contentType = getContentType(name);//讀取內(nèi)容類型 if (contentType != null) { response.setContentType(contentType);//重新設(shè)置內(nèi)容類型 } ……(省略部分代碼) try { //將讀取到的文件流以每次復(fù)制4096個字節(jié)的方式循環(huán)輸出 copy(is, response.getOutputStream()); } finally { is.close(); } return; } } } } 如果用戶請求的資源不是以/struts開頭——可能是.jsp文件,也可能是.html文件,則通過過濾器鏈繼續(xù)往下傳送,直到到達請求的資源為止。 如果getMapping()方法返回有效的ActionMapping對象,則被認為正在請求某個Action,將調(diào)用Dispatcher.serviceAction(request, response, servletContext, mapping)方法,該方法是處理Action的關(guān)鍵所在。上述過程的源代碼如清單15所示。 代碼清單15:FilterDispatcher.doFilter()方法 publicvoid doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; ServletContext servletContext = getServletContext(); String timerKey = "FilterDispatcher_doFilter: "; try { UtilTimerStack.push(timerKey); request = prepareDispatcherAndWrapRequest(request, response);//重新包裝request ActionMapping mapping; try { mapping = actionMapper.getMapping(request, dispatcher.getConfigurationManager());//得到存儲Action信息的ActionMapping對象 } catch (Exception ex) { ……(省略部分代碼) return; } if (mapping == null) {//如果mapping為null,則認為不是請求Action資源 String resourcePath = RequestUtils.getServletPath(request); if ("".equals(resourcePath) && null != request.getPathInfo()) { resourcePath = request.getPathInfo(); } //如果請求的資源以/struts開頭,則當作靜態(tài)資源處理 if (serveStatic && resourcePath.startsWith("/struts")) { String name = resourcePath.substring("/struts".length()); findStaticResource(name, request, response); } else { //否則,過濾器鏈繼續(xù)往下傳遞 chain.doFilter(request, response); } // The framework did its job here return; } //如果請求的資源是Action,則調(diào)用serviceAction方法。 dispatcher.serviceAction(request, response, servletContext, mapping); } finally { try { ActionContextCleanUp.cleanUp(req); } finally { UtilTimerStack.pop(timerKey); } } }
這段代碼的活動圖如圖18所示:
(圖18) 在Dispatcher.serviceAction()方法中,先加載Struts2的配置文件,如果沒有人為配置,則默認加載struts-default.xml、struts-plugin.xml和struts.xml,并且將配置信息保存在形如com.opensymphony.xwork2.config.entities.XxxxConfig的類中。 類com.opensymphony.xwork2.config.providers.XmlConfigurationProvider負責(zé)配置文件的讀取和解析, addAction()方法負責(zé)讀取<action>標簽,并將數(shù)據(jù)保存在ActionConfig中;addResultTypes()方法負責(zé)將<result-type>標簽轉(zhuǎn)化為ResultTypeConfig對象;loadInterceptors()方法負責(zé)將<interceptor>標簽轉(zhuǎn)化為InterceptorConfi對象;loadInterceptorStack()方法負責(zé)將<interceptor-ref>標簽轉(zhuǎn)化為InterceptorStackConfig對象;loadInterceptorStacks()方法負責(zé)將<interceptor-stack>標簽轉(zhuǎn)化成InterceptorStackConfig對象。而上面的方法最終會被addPackage()方法調(diào)用,將所讀取到的數(shù)據(jù)匯集到PackageConfig對象中,細節(jié)請參考代碼清單16。 代碼清單16:XmlConfigurationProvider.addPackage()方法 protected PackageConfig addPackage(Element packageElement) throws ConfigurationException { PackageConfig newPackage = buildPackageContext(packageElement); if (newPackage.isNeedsRefresh()) { return newPackage; } if (LOG.isDebugEnabled()) { LOG.debug("Loaded " + newPackage); } // add result types (and default result) to this package addResultTypes(newPackage, packageElement); // load the interceptors and interceptor stacks for this package loadInterceptors(newPackage, packageElement); // load the default interceptor reference for this package loadDefaultInterceptorRef(newPackage, packageElement); // load the default class ref for this package loadDefaultClassRef(newPackage, packageElement); // load the global result list for this package loadGlobalResults(newPackage, packageElement); // load the global exception handler list for this package loadGlobalExceptionMappings(newPackage, packageElement); // get actions NodeList actionList = packageElement.getElementsByTagName("action"); for (int i = 0; i < actionList.getLength(); i++) { Element actionElement = (Element) actionList.item(i); addAction(actionElement, newPackage); } // load the default action reference for this package loadDefaultActionRef(newPackage, packageElement); configuration.addPackageConfig(newPackage.getName(), newPackage); return newPackage; }
活動圖如圖19所示:
(圖19) 配置信息加載完成后,創(chuàng)建一個Action的代理對象——ActionProxy引用,實際上對Action的調(diào)用正是通過ActionProxy實現(xiàn)的,而ActionProxy又由ActionProxyFactory創(chuàng)建,ActionProxyFactory是創(chuàng)建ActionProxy的工廠。 注:ActionProxy和ActionProxyFactory都是接口,他們的默認實現(xiàn)類分別是DefaultActionProxy和DefaultActionProxyFactory,位于com.opensymphony.xwork2包下。 在這里,我們絕對有必要介紹一下com.opensymphony.xwork2.DefaultActionInvocation類,該類是對ActionInvocation接口的默認實現(xiàn),負責(zé)Action和截攔器的執(zhí)行。 在DefaultActionInvocation類中,定義了invoke()方法,該方法實現(xiàn)了截攔器的遞歸調(diào)用和執(zhí)行Action的execute()方法。其中,遞歸調(diào)用截攔器的代碼如清單17所示: 代碼清單17:調(diào)用截攔器,DefaultActionInvocation.invoke()方法的部分代碼 if (interceptors.hasNext()) { //從截攔器集合中取出當前的截攔器 final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next(); UtilTimerStack.profile("interceptor: "+interceptor.getName(), new UtilTimerStack.ProfilingBlock<String>() { public String doProfiling() throws Exception { //執(zhí)行截攔器(Interceptor)接口中定義的intercept方法 resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this); returnnull; } }); } 從代碼中似乎看不到截攔器的遞歸調(diào)用,其實是否遞歸完全取決于程序員對程序的控制,先來看一下Interceptor接口的定義: 代碼清單18:Interceptor.java publicinterface Interceptor extends Serializable { void destroy(); void init(); String intercept(ActionInvocation invocation) throws Exception; } 所有的截攔器必須實現(xiàn)intercept方法,而該方法的參數(shù)恰恰又是ActionInvocation,所以,如果在intercept方法中調(diào)用invocation.invoke(),代碼清單17會再次執(zhí)行,從Action的Intercepor列表中找到下一個截攔器,依此遞歸。下面是一個自定義截攔器示例: 代碼清單19:CustomIntercepter.java publicclass CustomIntercepter extends AbstractInterceptor { @Override public String intercept(ActionInvocation actionInvocation) throws Exception { actionInvocation.invoke(); return"李贊紅"; } } 截攔器的調(diào)用活動圖如圖20所示:
(圖20) 如果截攔器全部執(zhí)行完畢,則調(diào)用invokeActionOnly()方法執(zhí)行Action,invokeActionOnly()方法基本沒做什么工作,只調(diào)用了invokeAction()方法。 為了執(zhí)行Action,必須先創(chuàng)建該對象,該工作在DefaultActionInvocation的構(gòu)造方法中調(diào)用init()方法早早完成。調(diào)用過程是:DefaultActionInvocation()->init()->createAction()。創(chuàng)建Action的代碼如下: 代碼清單20:DefaultActionInvocation.createAction()方法 protectedvoid createAction(Map contextMap) { try { action = objectFactory.buildAction(proxy.getActionName(), proxy.getNamespace(), proxy.getConfig(), contextMap); } catch (InstantiationException e) { ……異常代碼省略 } } Action創(chuàng)建好后,輪到invokeAction()大顯身手了,該方法比較長,但關(guān)鍵語句實在很少,用心點看不會很難。 代碼清單20:DefaultActionInvocation.invokeAction()方法 protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception { //獲取Action中定義的execute()方法名稱,實際上該方法是可以隨便定義的 String methodName = proxy.getMethod(); String timerKey = "invokeAction: "+proxy.getActionName(); try { UtilTimerStack.push(timerKey); Method method; try { //將方法名轉(zhuǎn)化成Method對象 method = getAction().getClass().getMethod(methodName, new Class[0]); } catch (NoSuchMethodException e) { // hmm -- OK, try doXxx instead try { //如果Method出錯,則嘗試在方法名前加do,再轉(zhuǎn)成Method對象 String altMethodName = "do" + methodName.substring(0, 1).toUpperCase() + methodName.substring(1); method = getAction().getClass().getMethod(altMethodName, new Class[0]); } catch (NoSuchMethodException e1) { // throw the original one throw e; } } //執(zhí)行方法 Object methodResult = method.invoke(action, new Object[0]); //處理跳轉(zhuǎn) if (methodResult instanceof Result) { this.result = (Result) methodResult; returnnull; } else { return (String) methodResult; } } catch (NoSuchMethodException e) { ……省略異常代碼 } finally { UtilTimerStack.pop(timerKey); } } 剛才使用了一段插述,我們繼續(xù)回到ActionProxy類。 我們說Action的調(diào)用是通過ActionProxy實現(xiàn)的,其實就是調(diào)用了ActionProxy.execute()方法,而該方法又調(diào)用了ActionInvocation.invoke()方法。歸根到底,最后調(diào)用的是DefaultActionInvocation.invokeAction()方法。 以下是調(diào)用關(guān)系圖: 其中: ActionProxy:管理Action的生命周期,它是設(shè)置和執(zhí)行Action的起始點。 ActionInvocation:在ActionProxy層之下,它表示了Action的執(zhí)行狀態(tài)。它持有Action實例和所有的Interceptor 以下是serviceAction()方法的定義: 代碼清單21:Dispatcher.serviceAction()方法 publicvoid serviceAction(HttpServletRequest request, HttpServletResponse response, ServletContext context, ActionMapping mapping) throws ServletException { Map<String, Object> extraContext = createContextMap(request, response, mapping, context); // If there was a previous value stack, then create a new copy and pass it in to be used by the new Action ValueStack stack = (ValueStack) request.getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY); if (stack != null) { extraContext.put(ActionContext.VALUE_STACK, ValueStackFactory.getFactory().createValueStack(stack)); } String timerKey = "Handling request from Dispatcher"; try { UtilTimerStack.push(timerKey); String namespace = mapping.getNamespace(); String name = mapping.getName(); String method = mapping.getMethod(); Configuration config = configurationManager.getConfiguration(); ActionProxy proxy = config.getContainer().getInstance(ActionProxyFactory.class).createActionProxy( namespace, name, extraContext, true, false); proxy.setMethod(method); request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack()); // if the ActionMapping says to go straight to a result, do it! if (mapping.getResult() != null) { Result result = mapping.getResult(); result.execute(proxy.getInvocation()); } else { proxy.execute(); } // If there was a previous value stack then set it back onto the request if (stack != null) { request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack); } } catch (ConfigurationException e) { LOG.error("Could not find action or result", e); sendError(request, response, context, HttpServletResponse.SC_NOT_FOUND, e); } catch (Exception e) { thrownew ServletException(e); } finally { UtilTimerStack.pop(timerKey); } } 最后,通過Result完成頁面的跳轉(zhuǎn)。 3.4 本小節(jié)總結(jié)總體來講,Struts2的工作機制比Struts1.x要復(fù)雜很多,但我們不得不佩服Struts和WebWork開發(fā)小組的功底,代碼如此優(yōu)雅,甚至能夠感受看到兩個開發(fā)小組心神相通的默契。兩個字:佩服。 以下是Struts2運行時調(diào)用方法的順序圖:
(圖21) 四、 總結(jié)閱讀源代碼是一件非常辛苦的事,對讀者本身的要求也很高,一方面要有扎實的功底,另一方面要有超強的耐力和恒心。本章目的就是希望能幫助讀者理清一條思路,在必要的地方作出簡單的解釋,達到事半功倍的效果。 當然,筆者不可能為讀者解釋所有類,這也不是我的初衷。Struts2+xwork一共有700余類,除了為讀者做到現(xiàn)在的這些,已無法再做更多的事情。讀者可以到Struts官方網(wǎng)站下載幫助文檔,慢慢閱讀和理解,相信會受益頗豐。 本章并不適合java語言初學(xué)者或者對java博大精深的思想理解不深的讀者閱讀,這其中涉及到太多的術(shù)語和類的使用,特別不要去鉆牛角尖,容易使自信心受損?;靖闱宄?/span>Struts2的使用之后,再回過頭來閱讀本章,對一些知識點和思想也許會有更深的體會。 如果讀者的java功底比較渾厚,而且對Struts2充滿興趣,但又沒太多時間研究,不妨仔細閱讀本章,再對照Struts的源代碼,希望對您有所幫助。 |
|