struts2技巧之插件化开发

作为一家传统行业公司,作为利润大户的产品技术架构还是struts2的。随着SpringMVC越来越火,我也就没有将精力分配到struts2底层的研究,不过最近公司打算对该利润大户项目作一点非破坏性升级。插件化开发被首当其冲地提了出来。

1. 概述

基本要求如下:
1. 插件化组件对主体项目有完全的知情权,可以使用主体项目中所有的public资源。
2. 主体项目对插件化组件完全无感知。
3. 插件化组件可以参与主体项目的基本生命周期。
4. 插件化组件以JAR的形式出现在主体项目的WEB-INF/lib文件夹下。
5. 当插件化组件JAR出现在lib文件夹下时,主体项目就拥有了该组件提供的功能,否则就没有该功能。

2. 实现

难点大概是如下几个:
1. 插件化组件的struts配置如何并入?
2. 插件化组件的持久层操作相关的Mybatis映射配置文件如何并入?
3. 插件化组件如何无缝参与主体项目的生命周期?

2.1 插件化组件的struts配置

因为本人之前对struts2的了解, 只有在两年前进行过一次helloworld的配置,所以这次的升级过程,我是直接跟着源代码走的。

在经过一番探索后,最终在XmlConfigurationProvider类的私有方法loadConfigurationFiles中发现了对<include />节点的解析操作。

if ("include".equals(nodeName)) {
    String includeFileName = child.getAttribute("file");
    // 出现通配符
    if (includeFileName.indexOf('*') != -1) {
        // handleWildCardIncludes(includeFileName, docs, child);
        // 启用工具类ClassPathFinder
        ClassPathFinder wildcardFinder = new ClassPathFinder();
        wildcardFinder.setPattern(includeFileName);
        Vector<String> wildcardMatches = wildcardFinder.findMatches();
        for (String match : wildcardMatches) {
            finalDocs.addAll(loadConfigurationFiles(match, child));
        }
    } else {
        finalDocs.addAll(loadConfigurationFiles(includeFileName, child));
    }
}

以上的解析过程很清晰地表明了struct2的<include />节点是支持通配符操作的。所以这个问题就非常迎刃而解了,我们只需要在主体项目进行一个约定性的通配符配置即可

<!-- 插件式模块加载 -->
<include file="config/struts-plugin-*.xml"></include>

仔细观察上面的源码,如果struts2使用该通配符没有加载到配置文件,将会直接跳过这段逻辑,这完全满足我们的要求。

2.2 插件化组件的Mybatis映射配置文件

基本思路和 2.1 一致。唯一需要注意的是为了防止冲突,推荐Mybatis映射文件中的namespace统一加上本插件化组件的名称为前缀

    // 加载插件组件的Mybatis映射配置文件
    private static final void loadMybatisMapperFiles4PluginComponents(final Configuration configuration)
            throws IOException {
        // 这里需要引入spring-core-xxx.jar 
        // 这里我是为了赶时间和省事; 如果有极端代码洁癖的话, 可以使用 2.1中出现的struts2源码里的ClassPathFinder来完成同样的工作。  
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();       
        Resource[] mapperLocations = resolver.getResources(configSpringClassPath(configuration));

        if (mapperLocations.length == 0) {
            return;
        }

        for (Resource mapperLocation : mapperLocations) {
            if (mapperLocation == null) {
                continue;
            }
            try {
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration,
                        mapperLocation.toString(), configuration.getSqlFragments());
                xmlMapperBuilder.parse();
            } catch (Exception e) {
                throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
            } finally {
                ErrorContext.instance().reset();
            }

            if (LOG.isDebugEnabled()) {
                LOG.debug("Parsed mapper file: '" + mapperLocation + "'");
            }
        }
    }

    // 这里就是所加载的Mybatis映射文件的路径规则了
    private static String configSpringClassPath(final Configuration configuration) {
        // config/mybatis/qysb/oracle/_salve/xxxMapper_oracle.xml
        final String dbType = configuration.getVariables().getProperty("dbtype", "oracle");
        return StringUtil.format("classpath*:config/mybatis/*/{}/*/*Mapper_{}.xml", dbType,dbType);
    }

同样的,如果加载不到,本段代码将不作任何操作。

2.3 插件化组件参与主体项目的生命周期 ####

相比较于上面两个问题,这个问题还是花费我一些时间去思考的,主要是为了找出一个比较好的最佳实践。

2.3.1 尝试1

最开始的时候,我是打算参考Servlet3.0中的ServletContainerInitializer契约,主项目通过扫描WEB-INF/lib下的JAR中实现了主项目规定的契约接口的实现类,进而回调插件化组件的自定义逻辑,使得插件化组件可以参与生命周期。

最终放弃这种方式,因为在实际的测试中,发现扫描消耗的时间过长了,而参考Tomcat类似实现时,发现需要花费一些时间。

2.3.2 尝试2

然后又尝试借助struts2的配置文件,看是否可以在代码中读取到插件化组件声明在struts-plugin-xx.xml中节点内容。

最终也是放弃了这个方法,因为在阅读struts2源码之后,加上网上查找到的资料,发现struts2对配置文件的解析工作并没有留出相应的扩展点,其对节点的解析完全被内置了。

2.3.2 尝试3(终版)

最终选择了使用Java SPI来完成这项功能。其实当想到这种方式的时候,这个问题就不是问题了,具体的细节和实现过程就不细说了。

// 回调其他 插件化组件 的XxxLifeCycle实现
private void init_invokeXxxLifeCycle(ServletContext servletContext) {
    // 使用Java的SPI来回调插件化组件的自定义需求
    ServiceLoader<XxxLifeCycle> loader = ServiceLoader.load(XxxLifeCycle.class);
       for (XxxLifeCycle service : loader) {
           service.init(servletContext);
       }
}

还需要提及的是,Tomcat中类似的实现是在类WebappServiceLoader<T>中(不过通过观察其实现,发现其并不是使用了Java源生的SPI方式——使用ServiceLoader类。)

4. 疑难杂症

  1. 关于使用JAR承载插件之后,开发时静态资源的刷新问题可以参见本人之前的文章。
  1. Java中的SPI(Service Provider Interface)介绍及示例
  2. Java SPI思想梳理
  3. .NET中的 IServiceProvider 接口
  4. Tomcat中的类WebappServiceLoader<T>

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/80043290