struts2请求处理过程源代码分析(4)
2013-04-02 18:56 阅读(178)

接着上文,


public void 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 {

            // FIXME: this should be refactored better to not duplicate work with the action invocation
            ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
            ActionContext ctx = new ActionContext(stack.getContext());
            ActionContext.setContext(ctx);

            UtilTimerStack.push(timerKey);
            request = prepareDispatcherAndWrapRequest(request, response);
            ActionMapping mapping;
            try {
                mapping = actionMapper.getMapping(request, dispatcher.getConfigurationManager());
            } catch (Exception ex) {
                log.error("error getting ActionMapping", ex);
                dispatcher.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);
                return;
            }

            if (mapping == null) {
                // 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();
                }

                if (staticResourceLoader.canHandle(resourcePath)) {
                    staticResourceLoader.findStaticResource(resourcePath, request, response);
                } else {
                    // this is a normal request, let it pass through
                    chain.doFilter(request, response);
                }
                // The framework did its job here
                return;
            }

            dispatcher.serviceAction(request, response, servletContext, mapping);

        } finally {
            try {
                ActionContextCleanUp.cleanUp(req);
            } finally {
                UtilTimerStack.pop(timerKey);
            }
        }
    }

定义一个ActionMapping类型的引用mapping。ActionMapping的功能很简单就是存放从请求url中解析出来的命名空间、action名、方法名等,源码定义:


public class ActionMapping {

    private String name;
    private String namespace;
    private String method;
    private String extension;
    private Map params;
    private Result result;
    .
    .
    .
    //省略
}

name是action的名称,namespace是命名空间,method是action的方法名,extension存放后缀(如果存在的话,如:.action)。params注释里写的是额外的参数,但我在源码中没有看到使用在啥地方了,所以我也不知道是干啥用的。result存放返回的result类型,但这个字段一般是空的,注意:一旦在ActionMapping中的result被赋值了,那么当前action的method方法就不会执行了,而直接跳转到result所指的路径下。下文的分析中会遇到这种情况。


public void 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 {

            // FIXME: this should be refactored better to not duplicate work with the action invocation
            ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
            ActionContext ctx = new ActionContext(stack.getContext());
            ActionContext.setContext(ctx);

            UtilTimerStack.push(timerKey);
            request = prepareDispatcherAndWrapRequest(request, response);
            ActionMapping mapping;
            try {
                mapping = actionMapper.getMapping(request, dispatcher.getConfigurationManager());
            } catch (Exception ex) {
                log.error("error getting ActionMapping", ex);
                dispatcher.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);
                return;
            }

            if (mapping == null) {
                // 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();
                }

                if (staticResourceLoader.canHandle(resourcePath)) {
                    staticResourceLoader.findStaticResource(resourcePath, request, response);
                } else {
                    // this is a normal request, let it pass through
                    chain.doFilter(request, response);
                }
                // The framework did its job here
                return;
            }

            dispatcher.serviceAction(request, response, servletContext, mapping);

        } finally {
            try {
                ActionContextCleanUp.cleanUp(req);
            } finally {
                UtilTimerStack.pop(timerKey);
            }
        }
    }
mapping = actionMapper.getMapping(request, dispatcher.getConfigurationManager())句生成ActionMapping实例,并解析url填充到ActionMapping中。actionMapper是ActionMapping的生成器,在struts-default.xml中默认的配置是org.apache.struts2.dispatcher.mapper.DefaultActionMapper类。方法参数request是请求对象,这个请求对象是经过struts2封装后的。还有通过dispatcher.getConfigurationManager()获得的配置管理器,通过配置管理器可以获得配置对象configuration。F5进入actionMapper.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);

        if (mapping.getName() == null) {
            return null;
        }

        parseActionName(mapping);

        return mapping;
    }

第1句先生成个ActionMapping实例,最后句返回这个实例,中间的代码就是解析url填充到ActionMapping中了。第2句通过getUri()方法从request中获得uri,这样下面才能根据这个uri解析出命名空间、action名称、action方法名等。F5进入getUri():


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());
    }

第1句request.getAttribute("javax.servlet.include.servlet_path"),从request的属性"javax.servlet.include.servlet_path"获得servlet_path。我们知道在servlet中的RequestDispatcher有俩种跳转:forward和include。include是包含跳转,也就是说从页面1用include跳转到页面2时,会将页面2的内容包含到或插入到页面1中,即将俩个页面合二为一后在返回到客户端。而在跳转到页面2前web服务器(如:tomcat、resin等)会将页面2的相关信息保存到request属性中,属性名称是形如javax.servlet.include.XXX的形式, 如javax.servlet.include.servlet_path、javax.servlet.include.servlet_path.uri等。而此时通过request.getRequestURI()、request.getServletPath()获得的仍然是页面1的。所以struts2为了支持include跳转首先就要获得下被跳转页的地址即页面2的地址。如果此时获得的uri不为空(即是include跳转时)直接返回这个uri。如果此时的uri为空(即不是include跳转时)通过RequestUtils.getServletPath(request)进一步获得uri。F5进入RequestUtils.getServletPath(request)


public static String getServletPath(HttpServletRequest request) {
        String servletPath = request.getServletPath();
        
        String requestUri = request.getRequestURI();
        // Detecting other characters that the servlet container cut off (like anything after ';')
        if (requestUri != null && servletPath != null && !requestUri.endsWith(servletPath)) {
            int pos = requestUri.indexOf(servletPath);
            if (pos > -1) {
                servletPath = requestUri.substring(requestUri.indexOf(servletPath));
            }
        }
        
        if (null != servletPath && !"".equals(servletPath)) {
            return servletPath;
        }
        
        int startIndex = request.getContextPath().equals("") ? 0 : request.getContextPath().length();
        int endIndex = request.getPathInfo() == null ? requestUri.length() : requestUri.lastIndexOf(request.getPathInfo());

        if (startIndex > endIndex) { // this should not happen
            endIndex = startIndex;
        }

        return requestUri.substring(startIndex, endIndex);
    }

 RequestUtils类是专门处理request的通用工具类,目前只有一个方法 getServletPath()。前2句直接从request中获得servletPath和requestUri。下一句if句,判断requestUri的末尾是否和servletPath相同,不同则将末尾不同的补到servletPath尾。比如注释中已经给出了请求连接中末尾带有分号(;)时,web容器(tomcat、resin)在生成servletPath时,会将分号后的截掉,而uri中不会截掉。在下一句if,此时servletPath不为空直接返回。

RequestUtils类的注释中有这样句话"Deals with differences between servlet specs (2.2 vs 2.3+)",意思是处理servlet2.2与servlet2.3及以上版本的不同。我们知道servlet2.3中对2.2最大的改进之一是增加了过滤器(filter),相比于servlet,过滤器中是不满足requestURI=contextPath+servletPath+pathInfo的。所以下面的处理是针对servlet的。

下面的处理实际上是套用的公式:servletPath=requestURI - contextPath-pathInfo.具体看下:int startIndex = request.getContextPath().equals("") ? 0 : request.getContextPath().length()句生成requestURI 截取的开始下标,如果contextPath为"",则从requestURI 的开始处截取,此时startIndex=0.如果contextPath不为"",则从contextPath的长度为下标处截取(即从requestURI 中去掉contextPath)。int endIndex = request.getPathInfo() == null ? requestUri.length() : requestUri.lastIndexOf(request.getPathInfo())生成requestURI 截取的结束下标endIndex,即把requestUri末尾的pathInfo去掉(如果pathInfo存在的话)。if (startIndex > endIndex)句,正如注释(// this should not happen)所说这是不应该发生的,其实注意分析下上面俩句,发现这种情况也不可能发生。最后一句requestUri.substring(startIndex, endIndex)开始进行截取(即从requestUri中去掉contextPath和pathInfo),最后return。好,返回DefaultActionMapper类的getUri()方法,如下:


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());
    }
uri = RequestUtils.getServletPath(request);句返回uri后,如果uri不为空返回。否则继续处理,其实经过RequestUtils处理后uri必定不为空,如果为空下面的处理得到的uri也是空的,这个大家看下就行,我就直接返回了。返回到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);

        if (mapping.getName() == null) {
            return null;
        }

        parseActionName(mapping);

        return mapping;
    }

接着int indexOfSemicolon = uri.indexOf(";")句,检查uri中是否有分号,有的话去掉。uri = dropExtension(uri, mapping)句去掉uri中的后缀,如.action  F5进入:


protected String dropExtension(String name, ActionMapping mapping) {
        if (extensions == null) {
            return name;
        }
        for (String ext : extensions) {
            if ("".equals(ext)) {
                // This should also handle cases such as /foo/bar-1.0/description. It is tricky to
                // distinquish /foo/bar-1.0 but perhaps adding a numeric check in the future could
                // work
                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;
    }

方法的目的是去除uri末尾的后缀,如.action.因为此时后缀已经没有用了,在通过uri截取命名空间、action名称、action方法名时是不包括后缀的,所以此时要将后缀去掉。extensions属性是后缀的列表,是个list集合。extensions在定义时已经写死了俩个后缀action和"".如果要增加后缀可通过常量struts.action.extension配置。第一句if就不说了,直接for句。遍历extensions,if ("".equals(ext)) 时(也就是说uri没有后缀时),则直接返回uri(此时的uri就是参数name)。看下它是怎么判断,int index = name.lastIndexOf('.')检索uri中是否有".",如果index==-1,直接返回uri,说明此uri中没有后缀。否则通过name.indexOf('/', index) >= 0判断下"."后边是否还存在"/",有则说明"."并非是用做后缀的,而只是作为一个普通字符使用,如:/common/index.aa/aaaa  。else句,如果当时遍历的元素不是空(""),将截取后缀,并通过mapping.setExtension(ext)句将后缀放到extension属性中。在返回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);

        if (mapping.getName() == null) {
            return null;
        }

        parseActionName(mapping);

        return mapping;
    }
parseNameAndNamespace(uri, mapping, configManager)句将进行命名空间、action名称、action方法的截取,并将截取后的放入mapping中,F5进入:


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;
            // Find the longest matching namespace, defaulting to the default
            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);

            // Still none found, use root namespace if found
            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);
    }

上面方法位于:org.apache.struts2.dispatcher.mapper.DefaultActionMapper类中。是默认使用的,实际可通过配置文件中的<bean>元素进行配置。通过方法名parseNameAndNamespace就可判断出,是用于解析action名称和命名空间的。参数uri是通用资源标识符,实际上此时的uri已经是通过struts2在上面的方法以前经过处理的,格式是不带应用名(项目名)的形式,如:http://www.see-source.com/home/index!index 则此时的uri就是/home/index!index,http://localhost:8080/myproject/home/index!index此时的uri仍然是/home/index!index, 即不带myproject(应用名)。参数mapping 是用于存放解析出来的action名称、命名空间、方法名的一个封装对象。参数configManager是配置管理器,通过它可以获得系统的所有配置。mapping 、configManager因为与本篇文章文章没有太大关系,所以可不用理会。好,有了上面的大概说明后我们就来逐句分析下代码:

String namespace, name;
int lastSlash = uri.lastIndexOf("/");
首先定义存放命名空间、action名称的局部变量namespace, name。然后通过uri.lastIndexOf("/")句从后往前搜索第一个"/"的位置,将下标存于lastSlash 中。lastSlash 顾名思义"最后的斜杠"。


if (lastSlash == -1) {
    namespace = "";
    name = uri;
}

if (lastSlash == -1)即当uri中不存在"/"时,将命名空间namespace设为"",将name设为uri。例如当前的uri形式为index!list, 此时namespace为"",name为“index!list”(这个name后面还会进一步处理,将action名index,方法名list分别截出来)。但这种情况在实际使用中几乎看不到,我也想不出在什么情况下会出现,也许是我见识不够吧,有知道的朋友可以贴出来和大家分享下。


} 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);
} 
if (lastSlash == 0)即当uri以"/"开头时,并且uri中只存在一个"/"时,命名空间namespace设为"/",name设为去掉"/"后的uri。这种情况就是在http请求的URL地址中省略了命名空间,如http://localhost:8080/myproject/index!index,此时的uri为/index!index, namespace为"/",name为"index!index"。所以如果我们在地址中不写命名空间,将会被设置为"/",而不是“”。
else if (alwaysSelectFullNamespace) {
      // Simply select the namespace as everything before the last slash
       namespace = uri.substring(0, lastSlash);
       name = uri.substring(lastSlash + 1);
} 
其中参数alwaysSelectFullNamespace我们可以通过名字就能大概猜出来"允许采用完整的命名空间",即设置命名空间是否必须进行精确匹配,true必须,false可以模糊匹配,默认是false。这个参数可通过struts2的"struts.mapper.alwaysSelectFullNamespace"常量配置,如:<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />。当alwaysSelectFullNamespace为true时,将uri以lastSlash为分割,左边的为namespace,右边的为action的name。如:http://localhost:8080/myproject/home/index!index,此时uri为/home/index!index,lastSlash的当前值是5,这样namespace为"/home", name为index!index。


} 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;
            // Find the longest matching namespace, defaulting to the default
            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);

            // Still none found, use root namespace if found
            if (rootAvailable && "".equals(namespace)) {
                namespace = "/";
            }
        }
当上面的所有条件都不满足时,其中包括alwaysSelectFullNamespace 为false(命名空间进行模糊匹配),将由此部分处理,进行模糊匹配。第1句,通过configManager.getConfiguration()从配置管理器中获得配置对象Configuration,Configuration中存放着struts2的所有配置,形式是将xml文档的相应元素封装为java bean,如<package>元素被封装到PackageConfig类中,这个一会儿会用到。第2句按lastSlash将uri截取出prefix,之后将会拿prefix进行模糊匹配。第3句namespace = "",将命名空间暂时设为""。第4句创建并设置rootAvailable,rootAvailable作用是判断配置文件中是否配置了命名空间"/"(根命名空间),true为配置了,false未配置。下面for语句将会遍历我们配置的所有包(<package>),同时设置rootAvailable。第5句for,通过config.getPackageConfigs()获得所有已经配置的包,然后遍历。String ns = ((PackageConfig) cfg).getNamespace()获得当前包的命名空间ns,之后的if句是进行模糊匹配的核心,我摘出来单独说,如下:


if (ns != null && prefix.startsWith(ns) && (prefix.length() == ns.length() ||   prefix.charAt(ns.length()) == '/')) {
   if (ns.length() > namespace.length()) {
      namespace = ns;
   }
}
ns != null && prefix.startsWith(ns)这部分判断当ns不等于空并且ns是prefix的前缀。prefix.length() == ns.length()当二者长度相等时,结合前面部分就是ns是prefix的前缀并且二者长度相等,最终结论就是ns和prefix相等。如果前面的条件不成立,则说明prefix的长度大于ns。prefix.charAt(ns.length()) == '/')意思是prefix中与ns不相等的字符中的第一个字符必须是"/",也就是说,在命名空间采用斜杠分级的形式中,ns必须是prefix的某一子集,如:/common/home 是用户配置的命名空间,则在http的请求url中,/common/home/index1、/common/home/index2/common/home/index/aaa 都是正确的,都可以成功的匹配到/common/home,而/common/homeaa、/common/homea/aaa都是错误的。接着if (ns.length() > namespace.length()) 句,目的是找出字符长度最长的。因为命名空间采用的是分级的,则长度越长所表示的越精确,如/common/home/index比/common/home精确。之后将namespace = ns。好,这部分代码说完了,返回前一个代码,接着看:


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;
            // Find the longest matching namespace, defaulting to the default
            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);

            // Still none found, use root namespace if found
            if (rootAvailable && "".equals(namespace)) {
                namespace = "/";
            }
        }
if ("/".equals(ns)) 当我们配置了"/"这个命名空间时,将rootAvailable = true(表示配置了根命名空间)。name = uri.substring(namespace.length() + 1)句不涉及到命名空间就不说了。if (rootAvailable && "".equals(namespace))如果通过上面的for循环没有找到匹配的命名空间即namespace的值仍然是当初设置的"",但却配置了"/"时,将命名空间设为"/"。


经过上面的分析进行下总结:

(1). 如果请求url中没有命名空间时,将采用"/"作为命名空间。

(2). 当我们将常量 struts.mapper.alwaysSelectFullNamespace设为true时,那么请求url的命名空间必须和配置文件配置的完全相同才能匹配。

     当将常量 struts.mapper.alwaysSelectFullNamespace设为false时,那么请求url的命名空间和配置文件配置的可按模糊匹配。规则:

            a.如果配置文件中配置了/common 则/common、/common/home、/common/home/index都是可匹配的,即子命名空间可匹配父命名空间。

            b.如果对于某个url请求中的命名空间同时匹配了俩个或俩个以上的配置文件中配置的命名空间,则选字符最长的,如:当前请求的命名空间为/common/home/index/aaaa,  而我们在配置时同时配置               了/common/home、/common/home/index  则将会匹配命名空间最长的,即/common/home/index。

(3).最后,如果请求的命名空间在配置中没有匹配到时,将采用""作为命名空间。如果没有设置为""的命名空间将抛出404错误。