本文通过分析祥细的分析strut2.31的源代码来加深对struts2的认识。使我们在使用struts2框架的时候更有把握更得心应手。(独到的分析,非常祥细,原创)
struts2的入口就是一个StrutsPrepareAndExecuteFilter 过滤器,网上的也有很多分析struts2的文章了,但在filter入口这里却一笔略过,
其实我觉得先让大家都深入了解filter的原理这后,再一条线索往下,也许这样更能使你的思路更清淅,
所以我借助tomcat中filter的源代码从源头上开始分析,正所谓技术是日新月异的,也许哪天他的实现方式改变了,但是原理这东西是沉甸下来的,原理就是原理。
请耐心地看,也许我有哪些地方分析得不是很好,但我期望指正,分享学习也是一种快乐,让我们一起前进。
Servlet2.3中引入Filter其实它就是使用了职责链模式(Chain Of Responsibility)的抽象设计了。责链设计模式的意图就是使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
其实tomcat中管道和过滤器模式就是职责链模式的抽象,把它应用到软件体系架构中。
用一个通俗例子来说,就好比警察抓犯人,在案件发生后经侦察已经确认此案由一团伙共5个犯人所作,数量已经确定了,打个比方这个犯罪团伙是这样组成的,这5个犯人中只有一个主犯 E,其余四个从犯中只有D这个人认识并能找到主犯,D找了个只有他才认识的C犯,C又找了只有他才认识的B犯,B也是找了只有他才认识的A犯, 所以他们组成了一个犯罪团伙了。正好警察是从抓到 A开始的,接着A招供了B,B招供了C,C招供了D,D最后招供了主犯E了。(A->B->C->D->>E),呵呵,可能比喻得有点勉强了,但你能清楚了解就好,其实为什么突出有个主犯E呢,就好类比tomcat中的Filter 和servlet的执行顺序。(A->B->C->D)都好比如是Filter.doFilter(),链式的执行后,而主犯E就是最后用户定义的Servlet.service()了。
上面的都是比喻性地说明了一下,从下面开始就结合源代码来分析说明了。
init(FilterConfig):
当容器如tomcat启动的时候就会调用的filter初始化方法。在容器启动的时候。它加载应用程序中的配置描述符 web.xml 文件,解析过滤器配置信息。 并获取文件中指定的filter初始化参数。filter顺序是按<filter-mapping>的顺序依次排列保存下来,所以此时filter的数量 n 已经能被计算确定下来了。
在Tomcat处理请求的Servlet实例之前先要处理与之相关联的所有Filter的,在StandardWrapperValve类的#invoke()方法中调用filterChain.doFilter(request.getRequest(), response.getResponse()); 从这开始调用Filter了,
以下是在ApplicationFilterChain类两个方法的源代码:
public void doFilter(ServletRequest request, ServletResponse response) throws IOException,
ServletException {
if (Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController
.doPrivileged(new java.security.PrivilegedExceptionAction() {
public Object run() throws ServletException, IOException {
internalDoFilter(req, res);
return null;
}
});
} catch (PrivilegedActionException pe) {
......
}
} else {
internalDoFilter(request, response); //这个方法无论怎么样都会被调用
}
}
private void internalDoFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (pos < n) {
//n 就是web.xml定义的Filter的数量,init时已经计算出来,filter的排列顺序按<filter-mapping>的顺序已经定下来,
而且保存在filterConfig中了。
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = null;
try {
// 得到当前Filter
filter = filterConfig.getFilter();
support.fireInstanceEvent(InstanceEvent.BEFORE_FILTER_EVENT, filter, request,
response);
if (Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal = ((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[] { req, res, this };
SecurityUtil.doAsPrivilege("doFilter", filter, classType, args);
args = null;
} else {
// 这里调用Filter的#doFilter()方法。 注意看这个filter.doFilter(request, response, this); 中
//的this引用指向的就是ApplicationFilterChain类的对象,所以传递给Filter的chain实例就是ApplicationFilterChain类的对象。
filter.doFilter(request, response, this);
//还记得前面比喻的例子吗, 犯人A记住B,B->C,C->D吗,
//在Filter的#doFilter()方法里都会调用#chain.doFilter(request,response); 这个方法才能往下继续执行下去,
//就好比只要有一犯人记不住下一个犯人,这条线索就中断的不会继续往下了。
// 再来看这个chain的定义,它是ApplicationFilterChain类的的引用,而这个internalDoFilter(..)方法所在的类就是ApplicationFilterChain类中
//于是程序执行到Filter 中的chain.doFilter(request,response)相当于程序又再次调用本方法,也可以说是一种变相的递归了。
}
support.fireInstanceEvent(InstanceEvent.AFTER_FILTER_EVENT, filter, request,
response);
} catch (Exception e) {
...............
} ........省略
return; //说明只要符合if(pos < n)这条件,这方法只执行到这了。
}
try {
if (Globals.STRICT_SERVLET_COMPLIANCE) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
// 当Filter全部调用完毕后,而且在最后的一个Filter中有调用#chain.doFilter(request,response);这一行,
//才会把请求真正的传递给Servlet了,调用它的#service()方法。
support.fireInstanceEvent(InstanceEvent.BEFORE_SERVICE_EVENT, servlet, request,
response);
if ((request instanceof HttpServletRequest)
&& (response instanceof HttpServletResponse)) {
if (Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal = ((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[] { req, res };
SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args,
principal);
args = null;
} else {
servlet.service((HttpServletRequest) request, (HttpServletResponse) response);
}
} else {
servlet.service(request, response);
}
support.fireInstanceEvent(InstanceEvent.AFTER_SERVICE_EVENT, servlet, request,
response);
} catch (Exception e) {
............
}............
finally {
if (Globals.STRICT_SERVLET_COMPLIANCE) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
}
在Tomcat怎么处理Filter的流程说完了,接下来开始讨论Struts2的了,
Struts2就是由一个 StrutsPrepareAndExecuteFilter也就是一个filter作为控制器入口。所以它要在web.xml中配置。当浏览器发来的请求到达这个 StrutsPrepareAndExecuteFilter了,
就会调用 它的#doFilter()方法:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
prepare.setEncodingAndLocale(request, response);//设置编码和Locale
prepare.createActionContext(request, response);
prepare.assignDispatcherToThread();//获取Dispatcher的实例然后设置ThreadLocal<Dispatcher> instance中,可见Struts2框架为每一个线程都提供了一个Dispatcher对象,所以在编写Action的时候不需要考虑多线程的问题了。
if ( excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) {
chain.doFilter(request, response);
} else {
/* (1) 对请求进行包装 */
request = prepare.wrapRequest(request);
/* (2) 获得Action Mapping ,其中包含namespace处理*/
ActionMapping mapping = prepare.findActionMapping(request, response, true);
/* (3)当Mapping为空时,检查是否访问的为静态资源 */
if (mapping == null) {
boolean handled = execute.executeStaticResourceRequest(request, response);
if (!handled) {
chain.doFilter(request, response);
}
} else {
/* (4) 如果请求的是action资源,这会执行此方法,在这方法中会调用很多相关的方法做了很多工作(例如execute)和拦截器执行等 */
execute.executeAction(request, response, mapping);
//注意到这里了吗,没有chain.doFilter(request, response);也就是说如果执行的请求的资源是Action,就不会再往下执行到Servlet.service()了。
}
}
} finally {
prepare.cleanupRequest(request);
}
}
从上面(1)(2)(3)(4)点分别分开重点分明;
(1) 对请求进行包装
(2) 获得Action Mapping 以及怎么处理namespace的。
(3)如何检查是否访问的为静态资源,还有如果是正常的请求如jsp,js等,则让它真接通过chain.doFilter(..)链下去。
(4)调用被请求的Action的执行方法。
下面形如依次分析说明了。
(1)对请求进行包装:request = prepare.wrapRequest(request);
public HttpServletRequest wrapRequest(HttpServletRequest oldRequest) throws ServletException {
HttpServletRequest request = oldRequest;
try {
request = dispatcher.wrapRequest(request, servletContext);
}catch{..}
}
以下是Dispatcher中#wrapRequest()源代码:
public HttpServletRequest wrapRequest(HttpServletRequest request, ServletContext servletContext) throws IOException {
// don't wrap more than once
if (request instanceof StrutsRequestWrapper) {// 判断request是否是StrutsRequestWrapper的对象,保证对request只包装一次。
return request;
}
String content_type = request.getContentType();
//判断Content-Type是否是multipart/form-data,如果是的话返回一个MultiPartRequestWrapper的对象处理文件上传,
//否则返回StrutsRequestWrapper的对象处理普通请求。
if (content_type != null && content_type.contains("multipart/form-data")) {
MultiPartRequest mpr = null;
//check for alternate implementations of MultiPartRequest
Set<String> multiNames = getContainer().getInstanceNames(MultiPartRequest.class);
if (multiNames != null) {
for (String multiName : multiNames) {
if (multiName.equals(multipartHandlerName)) {
mpr = getContainer().getInstance(MultiPartRequest.class, multiName);
}
}
}
if (mpr == null ) {
mpr = getContainer().getInstance(MultiPartRequest.class);
}
request = new MultiPartRequestWrapper(mpr, request, getSaveDir(servletContext));
} else {
request = new StrutsRequestWrapper(request);
}
return request;
}
(2) 获得Action Mapping 以及怎么处理namespace的。
ActionMapping mapping = prepare.findActionMapping(request, response, true);
//ActionMapping类,它内部封装了如下6个字段:
//ActionMapping类,它内部封装了如下6个字段:
public class ActionMapping {
private String name;// Action名
private String namespace;// Action所在的名称空间
private String method;// 执行方法
private String extension;//扩展名如(.action)
private Map<String, Object> params;// 可以通过set方法设置的参数
private Result result;// 返回的结果类型
}
public ActionMapping findActionMapping(HttpServletRequest request, HttpServletResponse response, boolean forceLookup) {
ActionMapping mapping = (ActionMapping) request.getAttribute(STRUTS_ACTION_MAPPING_KEY);
if (mapping == null || forceLookup) {
try {
mapping = dispatcher.getContainer().getInstance(ActionMapper.class).getMapping(request, dispatcher.getConfigurationManager());
if (mapping != null) {
request.setAttribute(STRUTS_ACTION_MAPPING_KEY, mapping);
}
} catch (Exception ex) {
.....
}
}
return mapping;
}
实质调用的是DefaultActionMapper类的getMapping(..)
public ActionMapping getMapping(HttpServletRequest request,
ConfigurationManager configManager) {
ActionMapping mapping = new ActionMapping();
String uri = getUri(request);//下面祥细介绍
int indexOfSemicolon = uri.indexOf(";");
uri = (indexOfSemicolon > -1) ? uri.substring(0, indexOfSemicolon) : uri;
uri = dropExtension(uri, mapping);//下面祥细介绍
if (uri == null) {
return null;
}
parseNameAndNamespace(uri, mapping, configManager);//下面祥细介绍
handleSpecialParameters(request, mapping);//此方法用于处理Struts框架定义的四种特殊的prefix:Method prefix,Action prefix,Redirect prefix,Redirect-action prefix
if (mapping.getName() == null) {
return null;
}
parseActionName(mapping);
return mapping;
}
protected String getUri(HttpServletRequest request) {
// handle http dispatcher includes.
String uri = (String) request
.getAttribute("javax.servlet.include.servlet_path");
if (uri != null) {
return uri;
}
uri = RequestUtils.getServletPath(request);
if (uri != null && !"".equals(uri)) {
return uri;
}
uri = request.getRequestURI();
return uri.substring(request.getContextPath().length());
}
getUri()这个方法首先判断请求是否来自于一个jsp的include,
如果是,那么请求request.getAttribute("javax.servlet.include.servlet_path")属性可以获得include的页面uri,
否则通过一般的方法获得请求的uri,最后返回去掉ContextPath的请求路径,
比如http://127.0.0.1:8080/struts2/text/index.jsp?param=1,返回的为/text/index.jsp。去掉了ContextPath和查询字符串等。
uri = dropExtension(uri);
负责去掉Action的"扩展名"(默认为"action")并设置在mapping里,源代码如下:
protected String dropExtension(String name, ActionMapping mapping) {
if (extensions == null) {
return name;
}
for (String ext : extensions) {
if ("".equals(ext)) {
int index = name.lastIndexOf('.');
if (index == -1 || name.indexOf('/', index) >= 0) {
return name;
}
} else {
String extension = "." + ext;
if (name.endsWith(extension)) {
name = name.substring(0, name.length() - extension.length());
mapping.setExtension(ext);//设置扩展名
return name;
}
}
}
return null;
}
此方法用于解析Action的名称和命名空间,并赋给ActionMapping对象。源代码如下:
protected void parseNameAndNamespace(String uri, ActionMapping mapping,
ConfigurationManager configManager) {
String namespace, name;
int lastSlash = uri.lastIndexOf("/");
//首先如果经过前面处理后的uri为空“” ,就如请求contextPath/后就没有“/”了,则设置 namespace = "";
if (lastSlash == -1) {
namespace = "";
name = uri;
} else if (lastSlash == 0) {
//如果经过前面处理后的uri为/everything ,则设置 namespace = "/";
namespace = "/";
name = uri.substring(lastSlash + 1);
} else if (alwaysSelectFullNamespace) {
// alwaysSelectFullNamespace默认为false,代表是否将最后一个"/"前的字符串作为名称空间。
namespace = uri.substring(0, lastSlash);// 获得字符串 namespace
name = uri.substring(lastSlash + 1);// 获得字符串 name
} else {
//尝试去找在.xml中已经定义好的namespace,默认为空""字符。
Configuration config = configManager.getConfiguration();
String prefix = uri.substring(0, lastSlash);
namespace = "";
boolean rootAvailable = false;
// Find the longest matching namespace, defaulting to the default
//按最长匹配原则,如果struts.xml中的配置的有几个package并且namespace分别为 (如namespace=/first 。namespace=/first/second)
//请求的uri如果是/first/second/text.action 则先找到最长匹配namespace=/first/second的package
for (Object cfg : config.getPackageConfigs().values()) {
String ns = ((PackageConfig) cfg).getNamespace();
if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() || prefix.charAt(ns.length()) == '/')) {
if (ns.length() > namespace.length()) {
namespace = ns;
}
}
if ("/".equals(ns)) {
rootAvailable = true;
}
}
name = uri.substring(namespace.length() + 1);
//// allowSlashesInActionNames代表是否允许"/"出现在Action的名称中,默认为false
if (rootAvailable && "".equals(namespace)) {
namespace = "/";
}
}
if (!allowSlashesInActionNames && name != null) {
int pos = name.lastIndexOf('/');
if (pos > -1 && pos < name.length() - 1) {
name = name.substring(pos + 1);
}
}
mapping.setNamespace(namespace);
mapping.setName(name);
}
其实当这次namespace设置在mapping中后,在execute.executeAction(request, response, mapping)处理流程中会去相应的package中查找请求的action,
如果根据这次的namespace中对应的package中没有找到请求的action,会再次在DefaultConfiguration#getActionConfig(String namespace, String name)
中去找这个namespace的config,如果还是没有在这个namespace中找到对应name的Action则最终去默认为""空字符串的namespace中找。
public synchronized ActionConfig getActionConfig(String namespace, String name) {
ActionConfig config = findActionConfigInNamespace(namespace, name);
// try wildcarded namespaces
if (config == null) {
NamespaceMatch match = namespaceMatcher.match(namespace);
if (match != null) {
config = findActionConfigInNamespace(match.getPattern(), name);
// If config found, place all the matches found in the namespace processing in the action's parameters
if (config != null) {
config = new ActionConfig.Builder(config)
.addParams(match.getVariables())
.build();
}
}
}
//如果还是找不到在namespace的配置Actionconfig,则去namespace为""空的package中找Action为name为ActionConfig
if ((config == null) && (namespace != null) && (!"".equals(namespace.trim()))) {
config = findActionConfigInNamespace("", name);
}
return config;
}
//解析ActionName,并通过是否存在动态方法调用如:"name!method"形式的action调用
protected ActionMapping parseActionName(ActionMapping mapping) {
if (mapping.getName() == null) {
return mapping;
}
if (allowDynamicMethodCalls) {//如果允许动态方法调用,则去判断ActionName中是否为name!method形式,
//如果有!分隔则解析出name和method并保存在mapping;
String name = mapping.getName();
int exclamation = name.lastIndexOf("!");
if (exclamation != -1) {
mapping.setName(name.substring(0, exclamation));
mapping.setMethod(name.substring(exclamation + 1));
}
}
return mapping;//此时mapping的6个属性已经组装完成了,就返回;
}
(3)如何检查是否访问的为静态资源,还有如果是正常的请求如jsp等,
则让它真接通过chain.doFilter(..)链下去直到它会调用Servlet.service(),注:jsp最终也被解析为一个Servlet。
一就如的doFilter()中的源代码:
if (mapping == null) { //当前面获取mapping后还是null时,则检查是否访问的为静态资源
boolean handled = execute.executeStaticResourceRequest(request, response);
if (!handled) {
//请求如.jsp .html .js,让它通过则让给容器如tomcat处理。
chain.doFilter(request, response);
}
}
public boolean executeStaticResourceRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
//该请求不是一个Action请求时,先查找是不是访问的为静态资源
String resourcePath = RequestUtils.getServletPath(request);
if ("".equals(resourcePath) && null != request.getPathInfo()) {
resourcePath = request.getPathInfo();
}
StaticContentLoader staticResourceLoader = dispatcher.getContainer().getInstance(StaticContentLoader.class);
if (staticResourceLoader.canHandle(resourcePath)) {
staticResourceLoader.findStaticResource(resourcePath, request, response);
//如果访问的为静态资源则The framework did its job here
return true;
} else {
//如果是一个正常的请求如.jsp .html .js,让它通过
return false;
}
}
(4)调用被请求的Action的执行方法。
-----由于太长了,所以分开在struts2的源代码分析及struts2的工作流程(二)