先来看下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); //创建action上下文 prepare.createActionContext(request, response); prepare.assignDispatcherToThread(); if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) { chain.doFilter(request, response); } else { request = prepare.wrapRequest(request); ActionMapping mapping = prepare.findActionMapping(request, response, true); //如果mapping为空,则认为不是调用action,会调用下一个过滤器链,直到获取到mapping才调用action if (mapping == null) { boolean handled = execute.executeStaticResourceRequest(request, response); if (!handled) { chain.doFilter(request, response); } } else { //执行action execute.executeAction(request, response, mapping); } } } finally { prepare.cleanupRequest(request); } }
下面对doFilter方法中的重点部分一一讲解:
(1)prepare.setEncodingAndLocale(request, response);
/** * Sets the request encoding and locale on the response */ public void setEncodingAndLocale(HttpServletRequest request, HttpServletResponse response) { dispatcher.prepare(request, response); }
/** * Prepare a request, including setting the encoding and locale. * * @param request The request * @param response The response */ public void prepare(HttpServletRequest request, HttpServletResponse response) { String encoding = null; if (defaultEncoding != null) { encoding = defaultEncoding; } // check for Ajax request to use UTF-8 encoding strictly http://www.w3.org/TR/XMLHttpRequest/#the-send-method if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) { encoding = "utf-8"; } Locale locale = null; if (defaultLocale != null) { locale = LocalizedTextUtil.localeFromString(defaultLocale, request.getLocale()); } if (encoding != null) { applyEncoding(request, encoding); } if (locale != null) { response.setLocale(locale); } if (paramsWorkaroundEnabled) { request.getParameter("foo"); // simply read any parameter (existing or not) to "prime" the request } }
(2)prepare.createActionContext(request, response)
我们回到StrutsPrepareAndExecuteFilter的doFilter方法,看到第10行代码:prepare.createActionContext(request, response);这是action上下文的创建,ActionContext是一个容器,这个容易主要存储request、session、application、parameters等相关信 息.ActionContext是一个线程的本地变量,这意味着不同的action之间不会共享ActionContext,所以也不用考虑线程安全问 题。其实质是一个Map,key是标示request、session、……的字符串,值是其对应的对象,我们可以看到com.opensymphony.xwork2.ActionContext类中时如下定义的:
static ThreadLocal<ActionContext> actionContext = new ThreadLocal<ActionContext>();
我们可以在Action中直接取得这些:
HttpServletResponse resp = ServletActionContext.getResponse();
/** * Creates the action context and initializes the thread local */ public ActionContext createActionContext(HttpServletRequest request, HttpServletResponse response) { ActionContext ctx; Integer counter = 1; Integer oldCounter = (Integer) request.getAttribute(CLEANUP_RECURSION_COUNTER); if (oldCounter != null) { counter = oldCounter + 1; } //此处是从ThreadLocal中获取此ActionContext变量 ActionContext oldContext = ActionContext.getContext(); if (oldContext != null) { // detected existing context, so we are probably in a forward ctx = new ActionContext(new HashMap<String, Object>(oldContext.getContextMap())); } else { ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); stack.getContext().putAll(dispatcher.createContextMap(request, response, null, servletContext)); //stack.getContext()返回的是一个Map<String,Object>,根据此Map构造一个ActionContext ctx = new ActionContext(stack.getContext()); } request.setAttribute(CLEANUP_RECURSION_COUNTER, counter); //将ActionContext存到ThreadLocal ActionContext.setContext(ctx); return ctx; }
上面第18行代码中dispatcher.createContextMap,如何封装相关参数:
public Map<String,Object> createContextMap(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping, ServletContext context) { // request map wrapping the http request objects Map requestMap = new RequestMap(request); // parameters map wrapping the http parameters. ActionMapping parameters are now handled and applied separately Map params = new HashMap(request.getParameterMap()); // session map wrapping the http session Map session = new SessionMap(request); // application map wrapping the ServletContext Map application = new ApplicationMap(context); //requestMap、params、session等Map封装成为一个上下文Map Map<String,Object> extraContext = createContextMap(requestMap, params, session, application, request, response, context); if (mapping != null) { extraContext.put(ServletActionContext.ACTION_MAPPING, mapping); } return extraContext; }
request = prepare.wrapRequest(request)
我们再次回到StrutsPrepareAndExecuteFilter的doFilter方法中,看到第15行:request = prepare.wrapRequest(request);这一句是对request进行包装的,我们看下prepare的wrapRequest方法:
public HttpServletRequest wrapRequest(HttpServletRequest oldRequest) throws ServletException { HttpServletRequest request = oldRequest; try { // Wrap request first, just in case it is multipart/form-data // parameters might not be accessible through before encoding (ww-1278) request = dispatcher.wrapRequest(request, servletContext); } catch (IOException e) { throw new ServletException("Could not wrap servlet request with MultipartRequestWrapper!", e); } return request; }
由第6行我们可以看到它里面调用的是dispatcher的wrapRequest方法,并且将servletContext对象也传进去了,我们看下dispatcher的wrapRequest:
public HttpServletRequest wrapRequest(HttpServletRequest request, ServletContext servletContext) throws IOException { // don't wrap more than once if (request instanceof StrutsRequestWrapper) { return request; } String content_type = request.getContentType(); //如果content_type是multipart/form-data类型,则将request包装成MultiPartRequestWrapper对象,否则包装成StrutsRequestWrapper对象 if (content_type != null && content_type.contains("multipart/form-data")) { MultiPartRequest mpr = getMultiPartRequest(); LocaleProvider provider = getContainer().getInstance(LocaleProvider.class); request = new MultiPartRequestWrapper(mpr, request, getSaveDir(servletContext), provider); } else { request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup); } return request; }
此次包装根据请求内容的类型不同,返回不同的对象,如果为multipart/form-data类型,则返回MultiPartRequestWrapper类型的对象,该对象服务于文件上传,否则返回StrutsRequestWrapper类型的对象,MultiPartRequestWrapper是StrutsRequestWrapper的子类,而这两个类都是HttpServletRequest接口的实现。
(4)ActionMapping mapping = prepare.findActionMapping(request, response, true)
包装request后,通过ActionMapper的getMapping()方法得到请求的Action,Action的配置信息存储在ActionMapping对象中,如StrutsPrepareAndExecuteFilter的doFilter方法中第16行:ActionMapping mapping = prepare.findActionMapping(request, response, true);我们找到prepare对象的findActionMapping方法:
public ActionMapping findActionMapping(HttpServletRequest request, HttpServletResponse response, boolean forceLookup) { //首先从request对象中取mapping对象,看是否存在 ActionMapping mapping = (ActionMapping) request.getAttribute(STRUTS_ACTION_MAPPING_KEY); //不存在就创建一个 if (mapping == null || forceLookup) { try { //首先创建ActionMapper对象,通过ActionMapper对象创建mapping对象 mapping = dispatcher.getContainer().getInstance(ActionMapper.class).getMapping(request, dispatcher.getConfigurationManager()); if (mapping != null) { request.setAttribute(STRUTS_ACTION_MAPPING_KEY, mapping); } } catch (Exception ex) { dispatcher.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex); } } return mapping; }
下面是ActionMapper接口的实现类DefaultActionMapper的getMapping()方法的源代码:
public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) { ActionMapping mapping = new ActionMapping(); //获得请求的uri,即请求路径URL中工程名以后的部分,如/userAction.action String uri = getUri(request); //修正url的带;jsessionid 时找不到的bug int indexOfSemicolon = uri.indexOf(";"); uri = (indexOfSemicolon > -1) ? uri.substring(0, indexOfSemicolon) : uri; //删除扩展名,如.action或者.do uri = dropExtension(uri, mapping); if (uri == null) { return null; } //从uri中分离得到请求的action名、命名空间。 parseNameAndNamespace(uri, mapping, configManager); //处理特殊的请求参数 handleSpecialParameters(request, mapping); //如果允许动态方法调用,即形如/userAction!getAll.action的请求,分离action名和方法名 return parseActionName(mapping); }
下面对getMapping方法中的重要部分一一讲解:
①:parseNameAndNamespace(uri, mapping, configManager)
我们主要看下第14行的parseNameAndNamespace(uri, mapping, configManager);这个方法的主要作用是分离出action名和命名空间:
protected void parseNameAndNamespace(String uri, ActionMapping mapping, ConfigurationManager configManager) { String namespace, name; int lastSlash = uri.lastIndexOf("/"); //最后的斜杆的位置 if (lastSlash == -1) { namespace = ""; name = uri; } else if (lastSlash == 0) { // ww-1046, assume it is the root namespace, it will fallback to // default // namespace anyway if not found in root namespace. namespace = "/"; name = uri.substring(lastSlash + 1); //允许采用完整的命名空间,即设置命名空间是否必须进行精确匹配 } else if (alwaysSelectFullNamespace) { // Simply select the namespace as everything before the last slash namespace = uri.substring(0, lastSlash); name = uri.substring(lastSlash + 1); } else { // Try to find the namespace in those defined, defaulting to "" Configuration config = configManager.getConfiguration(); String prefix = uri.substring(0, lastSlash); //临时的命名空间,将会用来进行匹配 namespace = "";//将命名空间暂时设为"" boolean rootAvailable = false;//rootAvailable作用是判断配置文件中是否配置了命名空间"/" // Find the longest matching namespace, defaulting to the default for (Object cfg : config.getPackageConfigs().values()) { //循环遍历配置文件中的package标签 String ns = ((PackageConfig) cfg).getNamespace(); //获取每个package标签的namespace属性 //进行匹配 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); // Still none found, use root namespace if found if (rootAvailable && "".equals(namespace)) { namespace = "/"; } } if (!allowSlashesInActionNames) { int pos = name.lastIndexOf('/'); if (pos > -1 && pos < name.length() - 1) { name = name.substring(pos + 1); } } //将分离后的acion名和命名空间保存到mapping对象 mapping.setNamespace(namespace); mapping.setName(cleanupActionName(name)); }
到上面代码的第14行,参数alwaysSelectFullNamespace我们可以通过名字就能大概猜出来"允许采用完整的命名空间",即设置命名空间是否必须进行精确匹配,true必须,false可以模糊匹配,默认是false。进行精确匹配时要求请求url中的命名空间必须与配置文件中配置的某个命名空间必须相同,如果没有找到相同的则匹配失败。这个参数可通过struts2的"struts.mapper.alwaysSelectFullNamespace"常量配置,如:<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />。当alwaysSelectFullNamespace为true时,将uri以lastSlash为分割,左边的为namespace,右边的为name。如:http://localhost:8080/myproject/home/actionName!method.action,此时uri为/home/actionName!method.action(不过前面把后缀名去掉了,变成/home/actionName!method),lastSlash的,当前值是5,这样namespace为"/home", name为actionName!method
下面是命名空间匹配规则的总结:
(1). 如果请求url中没有命名空间时,将采用"/"作为命名空间。
(2). 当我们将常量 struts.mapper.alwaysSelectFullNamespace设为true时,那么请求url的命名空间必须和配置文件配置的完全相同才能匹配。
当将常量 struts.mapper.alwaysSelectFullNamespace设为false时,那么请求url的命名空间和配置文件配置的可按模糊匹配。规则:
a.如果配置文件中配置了/common 而url中的命名空间/common、/common/home、/common/home/index等等都是可匹配的,即子命名空间可匹配父命名空间。
b.如果对于某个url请求中的命名空间同时匹配了俩个或俩个以上的配置文件中配置的命名空间,则选字符最长的,如:当前请求的命名空间为/common/home/index/aaaa, 而我们在配置时同时配置 了/common/home、/common/home/index 则将会匹配命名空间最长的,即/common/home/index。
(3).最后,如果请求的命名空间在配置中没有匹配到时,将采用""作为命名空间。如果没有设置为""的命名空间将抛出404错误。