从源码总结struts2命名空间的匹配规则

以前看帖子中经常发现有关于命名空间的问题,如:有关""与"/"的区别、模糊匹配的规律等,下面是我根据源码中分析、总结的规律,希望对大家有所帮助。如果存在异议,希望大家能够及时反映。

采用的版本是struts2-2.1.6。

struts2中的命名空间归结起来其实有三种:

第一种:空,即""。这是默认的配置,如果用户在配置文件中没有显示说明命名空间,将采用空("")。

第二中:"/",即根空间。可以在配置文件中直接配置。

第三种:"/任意字符",当然这里的任意字符当然是符合命名规则的。如:"/common","/common/user"等。


下面通过分析源码来看下如何从请求的url中分析出命名空间,以及如何利用此命名空间去和我们配置的命名空间进行匹配,通过一些匹配规则来检索出最终的命名空间。如下:


/**
     * Parses the name and namespace from the uri
     *
     * @param uri
     *            The uri
     * @param mapping
     *            The action mapping to populate
     */
    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名称和命名空间的。通过此方法可以从客户端请求的url中截取出命名空间,在利用截取的命名空间与配置文件中配置的已有的命名空间进行匹配,来最终确定采用哪个命名空间。参数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 (译为最后的斜杠)中。



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

if (lastSlash == -1)即当uri中不存在"/"时,将命名空间namespace设为"",将name设为uri。例如当前的uri形式为index!index, 此时namespace为"",name为“index!index”。但这种情况在实际使用中几乎看不到,我也想不出在什么情况下会出现,有知道的朋友可以贴出来和大家分享下。



 

} 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。进行精确匹配时要求请求url中的命名空间必须与配置文件中配置的某个命名空间必须相同,如果没有找到相同的则匹配失败。这个参数可通过struts2的"struts.mapper.alwaysSelectFullNamespace"常量配置,如:<constant name="struts.mapper.alwaysSelectFullNamespace" value="true" />。当alwaysSelectFullNamespace为true时,将uri以lastSlash为分割,左边的为namespace,右边的为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 而url中的命名空间/common、/common/home、/common/home/index等等都是可匹配的,即子命名空间可匹配父命名空间。

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

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

 


 

 



 


posted @ 2013-04-03 13:17  doliangzhe2  阅读(1636)  评论(0编辑  收藏  举报