以往对于Struts2都是从网上查阅其工作原理及用法,没有做过深入了解,近期由于工作需要,看了一下Struts2-core的源码,趁热打铁将其中的流程梳理一下,和大家分享。此次使用的版本为Struts2.5.2
1. Struts2入口:StrutsPrepareAndExecuteFilter
要使用Struts,需要在web.xml配置StrutsPrepareAndExecuteFilter,如图:
<filter>
<filter-name>action2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>action2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
StrutsPrepareAndExecuteFilter是一个过滤器,实现了StrutsStatics和Filter接口,对于过滤器,大家都很熟悉,用来拦截我们指定的请求,在doFilter方法中做一些处理,如下图,filter中主要包含init、doFilter、destroy方法。
2. 配置文件初始化:init方法
public void init(FilterConfig filterConfig) throws ServletException {
InitOperations init = new InitOperations();
Dispatcher dispatcher = null;
try {
FilterHostConfig config = new FilterHostConfig(filterConfig);
init.initLogging(config);
dispatcher = init.initDispatcher(config);
init.initStaticContentLoader(config, dispatcher);
prepare = new PrepareOperations(dispatcher);
execute = new ExecuteOperations(dispatcher);
this.excludedPatterns = init.buildExcludedPatternsList(dispatcher);
postInit(dispatcher, filterConfig);
} finally {
if (dispatcher != null) {
dispatcher.cleanUpAfterInit();
}
init.cleanup();
}
}
当程序启动的时候,web容器(如:tomcat)会调用init方法,对一些Struts的配置进行初始化,在代码中,init调用initDispatcher(config)方法初始化了dispatcher,dispatcher的作用是对来自于客户端的请求进行分发和处理,初始化的内容包括:init-param的参数,default.properties,struts-default.xml,struts-plugin.xml,struts.xml等等。篇幅有限,此处代码不再赘述,dispatcher初始化方法序列图如下:
3. action请求处理:doFilter方法
接下来是重头戏了,doFilter用来对拦截的请求做处理。首先先看下代码:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
try {
String uri = RequestUtils.getUri(request);
//判断url是否在exclude中,若存在则跳过此filter
if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) {
LOG.trace("Request {} is excluded from handling by Struts, passing request to other filters", uri);
chain.doFilter(request, response);
} else {
LOG.trace("Checking if {} is a static resource", uri);
//判断是否访问静态资源
boolean handled = execute.executeStaticResourceRequest(request, response);
if (!handled) {
LOG.trace("Assuming uri {} as a normal action", uri);
prepare.setEncodingAndLocale(request, response);
prepare.createActionContext(request, response);
prepare.assignDispatcherToThread();
request = prepare.wrapRequest(request);
//解析request,生成actionMapping
ActionMapping mapping = prepare.findActionMapping(request, response, true);
if (mapping == null) {
LOG.trace("Cannot find mapping for {}, passing to other filters", uri);
chain.doFilter(request, response);
} else {
LOG.trace("Found mapping {} for {}", mapping, uri);
//处理action请求
execute.executeAction(request, response, mapping);
}
}
}
} finally {
prepare.cleanupRequest(request);
}
}
在代码中,首先判断url是否配置了例外,若在excludedPatterns中,则跳过,直接执行后面的filter。否则会调用execute.executeStaticResourceRequest(request, response),判断请求的是否为静态资源。在doFilter中会把请求分为静态资源和action两类做处理
(1)静态资源访问
下面来看下executeStaticResourceRequest是如何处理的。
public boolean executeStaticResourceRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// there is no action in this request, should we look for a static resource?
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 {
// this is a normal request, let it pass through
return false;
}
}
如上述代码判断,核心代码仅两处:
1) staticResourceLoader.canHandle(resourcePath)
判断是否能处理,如下显而易见,请求中包含/struts/和/static/路径即认为是访问静态资源
public boolean canHandle(String resourcePath) {
return serveStatic && (resourcePath.startsWith("/struts/") || resourcePath.startsWith("/static/"));
}
2)staticResourceLoader.findStaticResource(resourcePath, request, response); 查找静态资源
public void findStaticResource(String path, HttpServletRequest request, HttpServletResponse response)
throws IOException {
String name = cleanupPath(path);
for (String pathPrefix : pathPrefixes) {
//找到静态资源路径
URL resourceUrl = findResource(buildPath(name, pathPrefix));
if (resourceUrl != null) {
InputStream is = null;
try {
//check that the resource path is under the pathPrefix path
String pathEnding = buildPath(name, pathPrefix);
if (resourceUrl.getFile().endsWith(pathEnding))
is = resourceUrl.openStream();
} catch (IOException ex) {
// just ignore it
continue;
}
//not inside the try block, as this could throw IOExceptions also
if (is != null) {
//获得输入流并处理
process(is, path, request, response);
return;
}
}
}
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
通过findResource找到静态资源路径,并获取到InputStream,调用process方法处理。(此处可以通过init-param配置packages,指定静态资源路径),如果允许缓存的话,在process方法中会根据报文头中的If-Modified-Since判断客户端缓存是否为最新的,若最新则直接返回304,若不是,则将输出流返回。
protected void process(InputStream is, String path, HttpServletRequest request, HttpServletResponse response) throws IOException {
if (is != null) {
Calendar cal = Calendar.getInstance();
// check for if-modified-since, prior to any other headers
long ifModifiedSince = 0;
try {
ifModifiedSince = request.getDateHeader("If-Modified-Since");
} catch (Exception e) {
LOG.warn("Invalid If-Modified-Since header value: '{}', ignoring", request.getHeader("If-Modified-Since"));
}
long lastModifiedMillis = lastModifiedCal.getTimeInMillis();
long now = cal.getTimeInMillis();
cal.add(Calendar.DAY_OF_MONTH, 1);
long expires = cal.getTimeInMillis();
//ifModifiedSince为客户端记录的最后访问时间
//个人认为应该是ifModifiedSince > lastModifiedMillis,为什么?
if (ifModifiedSince > 0 && ifModifiedSince <= lastModifiedMillis) {
// not modified, content is not sent - only basic
// headers and status SC_NOT_MODIFIED
response.setDateHeader("Expires", expires);
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
is.close();
return;
}
// set the content-type header
String contentType = getContentType(path);
if (contentType != null) {
response.setContentType(contentType);
}
if (serveStaticBrowserCache) {
// set heading information for caching static content
response.setDateHeader("Date", now);
response.setDateHeader("Expires", expires);
response.setDateHeader("Retry-After", expires);
response.setHeader("Cache-Control", "public");
response.setDateHeader("Last-Modified", lastModifiedMillis);
} else {
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "-1");
}
try {
copy(is, response.getOutputStream());
} finally {
is.close();
}
}
}
/**
* Copy bytes from the input stream to the output stream.
*
* @param input
* The input stream
* @param output
* The output stream
* @throws IOException
* If anything goes wrong
*/
protected void copy(InputStream input, OutputStream output) throws IOException {
final byte[] buffer = new byte[4096];
int n;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
}
output.flush();
}
(2)action请求处理
接下来继续回到doFilter方法中,若handled返回false,则会按照action进行处理。此处我们只需重点关注下面两行代码:
ActionMapping mapping = prepare.findActionMapping(request, response, true);
execute.executeAction(request, response, mapping);
1)ActionMapping
在findActionMapping中,调用ActionMapper的getMapping方法将request请求解析成ActionMapping,其中包含action的name,method,namespace等信息。
public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) {
ActionMapping mapping = new ActionMapping();
String uri = RequestUtils.getUri(request);
int indexOfSemicolon = uri.indexOf(";");
uri = (indexOfSemicolon > -1) ? uri.substring(0, indexOfSemicolon) : uri;
uri = dropExtension(uri, mapping);
if (uri == null) {
return null;
}
//获得action的name和namespace
parseNameAndNamespace(uri, mapping, configManager);
//获得parameters
handleSpecialParameters(request, mapping);
//parseActionName方法中,判断是否开启了DMI,开启后才会处理name!method请求
return parseActionName(mapping);
}
protected ActionMapping parseActionName(ActionMapping mapping) {
if (mapping.getName() == null) {
return null;
}
//判断是否开启了DMI(动态方法绑定)
if (allowDynamicMethodCalls) {
// handle "name!method" convention.
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;
}
值得注意的是,在2.5版本中,Struts默认关闭了DMI,可以通过设置<constant name="struts.enable.DynamicMethodInvocation" value="true"/>来开启,具体逻辑可参考代码片段中注释
2)executeAction
public void serviceAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping)
throws ServletException {
//创建上下文
Map<String, Object> extraContext = createContextMap(request, response, mapping);
// 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);
boolean nullStack = stack == null;
if (nullStack) {
ActionContext ctx = ActionContext.getContext();
if (ctx != null) {
stack = ctx.getValueStack();
}
}
if (stack != null) {
extraContext.put(ActionContext.VALUE_STACK, valueStackFactory.createValueStack(stack));
}
String timerKey = "Handling request from Dispatcher";
try {
UtilTimerStack.push(timerKey);
String namespace = mapping.getNamespace();
String name = mapping.getName();
String method = mapping.getMethod();
//生成actionProxy,并持有ActionInvocation的实例
ActionProxy proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
namespace, name, method, extraContext, true, false);
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 {
//执行action
proxy.execute();
}
// If there was a previous value stack then set it back onto the request
if (!nullStack) {
request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack);
}
} catch (ConfigurationException e) {
logConfigurationException(request, e);
sendError(request, response, HttpServletResponse.SC_NOT_FOUND, e);
} catch (Exception e) {
if (handleException || devMode) {
sendError(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
} else {
throw new ServletException(e);
}
} finally {
UtilTimerStack.pop(timerKey);
}
}
在executeAction中实际上调用的是dispatcher的serviceAction()方法,先会调用createContextMap创建上下文,然后调用ActionProxyFactory的createActionProxy)生成actionProxy,在创建过程中,会创建ActionInvocation,并持有它。
protected void prepare() {
String profileKey = "create DefaultActionProxy: ";
try {
UtilTimerStack.push(profileKey);
config = configuration.getRuntimeConfiguration().getActionConfig(namespace, actionName);
if (config == null && unknownHandlerManager.hasUnknownHandlers()) {
config = unknownHandlerManager.handleUnknownAction(namespace, actionName);
}
if (config == null) {
throw new ConfigurationException(getErrorMessage());
}
//若找不到method,默认执行excute方法
resolveMethod();
//判断method是否允许被执行
if (config.isAllowedMethod(method)) {
//获取配置的拦截器interceptors
invocation.init(this);
} else {
throw new ConfigurationException(prepareNotAllowedErrorMessage());
}
} finally {
UtilTimerStack.pop(profileKey);
}
}
在2.5版本中Struts增加了安全验证,会判断method是否允许被执行,在struts2-core的struts-default.xml中默认配置了execute,input,back,cancel,browse,save,delete,list,index,即只允许这些method执行。
<global-allowed-methods>execute,input,back,cancel,browse,save,delete,list,index</global-allowed-methods>
可以通过配置<global-allowed-methods>regex:.*</global-allowed-methods>放开限制。之后invocation的init方法会获取配置的interceptors,至此action的执行准备工作就完成了,接下来回到serviceAction中,看action是如何被执行的。
public String execute() throws Exception {
ActionContext nestedContext = ActionContext.getContext();
ActionContext.setContext(invocation.getInvocationContext());
String retCode = null;
String profileKey = "execute: ";
try {
UtilTimerStack.push(profileKey);
//关键代码,执行invocation
retCode = invocation.invoke();
} finally {
if (cleanupContext) {
ActionContext.setContext(nestedContext);
}
UtilTimerStack.pop(profileKey);
}
return retCode;
}
在serviceAction中会调用proxy.execute(),我们的拦截器和action将会在此方法中被执行。如上面代码片段,我们看到核心的代码是执行了invocation.invoke(),那么invoke又是如何处理的呢?
/**
* @throws ConfigurationException If no result can be found with the returned code
*/
public String invoke() throws Exception {
String profileKey = "invoke: ";
try {
UtilTimerStack.push(profileKey);
if (executed) {
throw new IllegalStateException("Action has already executed");
}
if (interceptors.hasNext()) {
//-----step 1 获得一个拦截器
final InterceptorMapping interceptor = interceptors.next();
String interceptorMsg = "interceptor: " + interceptor.getName();
UtilTimerStack.push(interceptorMsg);
try {
//-----step 2 执行拦截器
resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
} finally {
UtilTimerStack.pop(interceptorMsg);
}
} else {
//-----step 3 如果hasNext为false,执行action
resultCode = invokeActionOnly();
}
// this is needed because the result will be executed, then control will return to the Interceptor, which will
// return above and flow through again
if (!executed) {
if (preResultListeners != null) {
LOG.trace("Executing PreResultListeners for result [{}]", result);
for (Object preResultListener : preResultListeners) {
PreResultListener listener = (PreResultListener) preResultListener;
String _profileKey = "preResultListener: ";
try {
UtilTimerStack.push(_profileKey);
listener.beforeResult(this, resultCode);
}
finally {
UtilTimerStack.pop(_profileKey);
}
}
}
// now execute the result, if we're supposed to
if (proxy.getExecuteResult()) {
//-----step 4 最后执行result返回
executeResult();
}
executed = true;
}
return resultCode;
}
finally {
UtilTimerStack.pop(profileKey);
}
}
注意上述代码片段中的中文注释,主要涉及4个步骤,获得拦截器,执行拦截器,执行action,执行result,按照如此流程action请求就执行完了,但是多个拦截器的话,对于hasNext,我们并没有看到期望的递归或者循环将所有的拦截器执行,那么Struts是如何处理的呢?别急,我们先看拦截器的intercept方法是如何执行的,Interceptor的实现类有很多,随便拿一个来看吧,以ActionAutowiringInterceptor为例,看一下它的intercept方法,如下:
@Override
public String intercept(ActionInvocation invocation) throws Exception {
if (!initialized) {
ApplicationContext applicationContext = (ApplicationContext) ActionContext.getContext().getApplication().get(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
if (applicationContext == null) {
LOG.warn("ApplicationContext could not be found. Action classes will not be autowired.");
} else {
setApplicationContext(applicationContext);
factory = new SpringObjectFactory();
factory.setApplicationContext(getApplicationContext());
if (autowireStrategy != null) {
factory.setAutowireStrategy(autowireStrategy);
}
}
initialized = true;
}
if (factory != null) {
Object bean = invocation.getAction();
factory.autoWireBean(bean);
ActionContext.getContext().put(APPLICATION_CONTEXT, context);
}
return invocation.invoke();
}
我们先不关心拦截器要实现什么功能,在上述代码中会发现方法传递了invocation参数,并且在return之前执行了invocation.invoke(),如此就又回到了invovation中,会继续获取拦截器执行。那么结果就呼之欲出了,invocation和Interceptor通过传参和持有实例变量,从而互相调用来达到递归的效果。
之前看Struts的执行流程图一直有个误解,认为struts的拦截器会被执行两次,进入和退出都会执行一次拦截器。今天看到源码才明白,invoke前面的代码会在action之前执行,后面的代码会在action执行之后再被执行,这也就解释了上图中拦截器进出的顺序问题。执行顺序如下图所示:
在执行完所有的interceptor之后,就是执行action,然后执行executeResult返回了。在executeResult中随意看了一个VelocityResult类,大致流程就是获取模版,赋值,然后返回输出流之类。有兴趣的同学可以继续跟一下代码。
4. 总结
本文以web.xml为入口,对Struts2的执行流程进行了简单的介绍,不得不说Struts的代码还是很牛的,在看源码的过程中学到了很多知识,对Struts2的执行流程有了清晰的认识。第一次写这么长的文章,希望不是那么杂乱不堪,能对看此文章的人有所帮助。