【问题标题】:Modular Spring-based application模块化的基于 Spring 的应用程序
【发布时间】:2013-12-13 02:54:19
【问题描述】:

我希望允许用户在主项目中添加/刷新/更新/删除模块,而无需重新启动或重新部署。用户将能够编写自己的模块并将它们添加到主项目中。

从技术上讲,一个模块将是一个 JAR,它可能是“热启动”的,并且可能包含:

  • 弹簧控制器
  • 服务、ejb...
  • 资源(jsps、css、图像、javascripts...)

因此,当用户添加模块时,应用程序必须按照预期注册控制器、服务、ejb 和映射资源。当他移除时,应用程序会卸载它们。

很容易说。实际上似乎要困难得多。

目前,I did it using Servlet 3.0 and web-fragment.xml。主要问题是每次更新模块时都必须重新部署。我需要避免这种情况。

我阅读了一些关于 OSGi 的文档,但我不明白如何将它与我的项目链接,也不明白它如何按需加载/卸载。

有人可以引导我找到解决方案或想法吗?

我用什么:

  • Glassfish 3.1.2
  • Spring MVC 3.1.3
  • Spring 安全 3.1.3

谢谢。


编辑:

我现在可以说这是可能的。这是我要做的方式:

添加模块:

  1. 上传module.jar
  2. 处理文件,在模块文件夹中展开
  3. 关闭 Spring 应用上下文
  4. 在父类为 WebappClassLoader 的自定义类加载器中加载 JAR
  5. 在主项目中复制资源(也许可以找到替代方案,我希望但目前应该可行)
  6. 刷新 Spring 应用上下文

删除模块:

  1. 关闭 Spring 应用上下文
  2. 取消绑定自定义类加载器并让它进入 GC
  3. 移除资源
  4. 从模块文件夹 + jar 中删除文件(如果保留)
  5. 刷新 Spring 应用上下文

对于每个,Spring 必须扫描另一个文件夹而不是

domains/domain1/project/WEB-INF/classes
domains/domain1/project/WEB-INF/lib
domains/domain1/lib/classes

这实际上是我当前的问题。

从技术上讲,我发现 PathMatchingResourcePatternResolver 和 ClassPathScanningCandidateComponentProvider 参与其中。现在我需要告诉他们扫描特定的文件夹/类。

对于其余部分,我已经做了一些测试,它应该可以按预期工作。

不可能的一点:jar 中的 ejbs。

当我做了一些有用的事情时,我会发布一些资源。

【问题讨论】:

  • 为什么这个要求如此重要?这将很难用 Spring 来实现。
  • 其实主项目已经在生产中,不能在我们想要的时候取消部署。这就是为什么我想在不重新部署的情况下即时推送模块。我刚刚找到了一种刷新 Spring 应用程序上下文的方法(不需要像部署那样需要 30 秒的初始化)。现在我需要找到一种方法来加载并将 JAR 注入到他的类加载器中,以使 Spring 识别控制器(暂时没有考虑资源)。无论如何感谢您的回答,我也怀疑这很难实现:(。
  • 这绝对是 OSGI。
  • Spring DM 成为 Eclipse Gemini 蓝图。 (恕我直言,你不应该尝试做你正在做的事情——这真的很复杂)
  • 嗨。我查看了仅基于应用程序(不是 Web)的 Gemini Blueprint,并找到了基于 ..web 的 Gemini Web。但是我没有让它工作(缺少 OsgiWebXmlApplicationContext)所以我放弃了。我昨天回来看我的草稿代码,我只是成功在编辑中描述的“添加模块”部分。它目前仍处于草稿状态,我会在发布任何内容之前清理并完成我的代码。

标签: java spring jsp spring-mvc


【解决方案1】:

好的,我做到了,但我确实有太多资源无法在此处发布。我将逐步解释我是如何做到的,但不会发布对于普通熟练开发人员来说很简单的类加载部分。

我的代码目前不支持的一件事是上下文配置扫描。

首先,下面的解释取决于您的需求以及您的应用服务器。我使用 Glassfish 3.1.2,但没有找到如何配置自定义类路径:

  • 不再支持类路径前缀/后缀
  • 域的 java-config 上的 -classpath 参数不起作用
  • CLASSPATH 环境也不起作用

所以 GF3 的类路径中唯一可用的路径是:WEB-INF/classes、WEB-INF/lib...如果你找到了方法在您的应用服务器上,您可以跳过前 4 个步骤。

我知道 Tomcat 可以做到这一点。

第 1 步:创建自定义命名空间处理程序

使用其 XSD、spring.handlers 和 spring.schemas 创建一个自定义 NamespaceHandlerSupport。此命名空间处理程序将包含 <context:component-scan/> 的重新定义。

/**
* Redefine {@code component-scan} to scan the module folder in addition to classpath
* @author Ludovic Guillaume
*/
public class ModuleContextNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("component-scan", new ModuleComponentScanBeanDefinitionParser());
    }
}

XSD 仅包含 component-scan 元素,这是 Spring 的完美副本。

spring.handlers

http\://www.yourwebsite.com/schema/context=com.yourpackage.module.spring.context.config.ModuleContextNamespaceHandler

spring.schemas

http\://www.yourwebsite.com/schema/context/module-context.xsd=com/yourpackage/module/xsd/module-context.xsd

注意:我没有覆盖 Spring 默认命名空间处理程序,因为项目名称需要有一个大于“S”的字母。我想避免这种情况,所以我创建了自己的命名空间。

第 2 步:创建解析器

这将由上面创建的命名空间处理程序初始化。

/**
 * Parser for the {@code <module-context:component-scan/>} element.
 * @author Ludovic Guillaume
 */
public class ModuleComponentScanBeanDefinitionParser extends ComponentScanBeanDefinitionParser {
    @Override
    protected ClassPathBeanDefinitionScanner createScanner(XmlReaderContext readerContext, boolean useDefaultFilters) {
        return new ModuleBeanDefinitionScanner(readerContext.getRegistry(), useDefaultFilters);
    }
}

第 3 步:创建扫描仪

这是自定义扫描仪,它使用与ClassPathBeanDefinitionScanner 相同的代码。唯一更改的代码是String packageSearchPath = "file:" + ModuleManager.getExpandedModulesFolder() + "/**/*.class";。

ModuleManager.getExpandedModulesFolder() 包含一个绝对 URL。例如:C:/&lt;project&gt;/modules/.

/**
 * Custom scanner that detects bean candidates on the classpath (through {@link ClassPathBeanDefinitionScanner} and on the module folder.
 * @author Ludovic Guillaume
 */
public class ModuleBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
    private ResourcePatternResolver resourcePatternResolver;
    private MetadataReaderFactory metadataReaderFactory;

    /**
     * @see {@link ClassPathBeanDefinitionScanner#ClassPathBeanDefinitionScanner(BeanDefinitionRegistry, boolean)}
     * @param registry
     * @param useDefaultFilters
     */
    public ModuleBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
        super(registry, useDefaultFilters);

        try {
            // get parent class variable
            resourcePatternResolver = (ResourcePatternResolver)getResourceLoader();

            // not defined as protected and no getter... so reflection to get it
            Field field = ClassPathScanningCandidateComponentProvider.class.getDeclaredField("metadataReaderFactory");
            field.setAccessible(true);
            metadataReaderFactory = (MetadataReaderFactory)field.get(this);
            field.setAccessible(false);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Scan the class path for candidate components.<br/>
     * Include the expanded modules folder {@link ModuleManager#getExpandedModulesFolder()}.
     * @param basePackage the package to check for annotated classes
     * @return a corresponding Set of autodetected bean definitions
     */
    @Override
    public Set<BeanDefinition> findCandidateComponents(String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>(super.findCandidateComponents(basePackage));

        logger.debug("Scanning for candidates in module path");

        try {
            String packageSearchPath = "file:" + ModuleManager.getExpandedModulesFolder() + "/**/*.class";

            Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
            boolean traceEnabled = logger.isTraceEnabled();
            boolean debugEnabled = logger.isDebugEnabled();

            for (Resource resource : resources) {
                if (traceEnabled) {
                    logger.trace("Scanning " + resource);
                }
                if (resource.isReadable()) {
                    try {
                        MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);

                        if (isCandidateComponent(metadataReader)) {
                            ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                            sbd.setResource(resource);
                            sbd.setSource(resource);

                            if (isCandidateComponent(sbd)) {
                                if (debugEnabled) {
                                    logger.debug("Identified candidate component class: " + resource);
                                }
                                candidates.add(sbd);
                            }
                            else {
                                if (debugEnabled) {
                                    logger.debug("Ignored because not a concrete top-level class: " + resource);
                                }
                            }
                        }
                        else {
                            if (traceEnabled) {
                                logger.trace("Ignored because not matching any filter: " + resource);
                            }
                        }
                    }
                    catch (Throwable ex) {
                        throw new BeanDefinitionStoreException("Failed to read candidate component class: " + resource, ex);
                    }
                }
                else {
                    if (traceEnabled) {
                        logger.trace("Ignored because not readable: " + resource);
                    }
                }
            }
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
        }

        return candidates;
    }
}

第 4 步:创建自定义资源缓存实现

这将允许 Spring 从类路径中解析您的模块类。

public class ModuleCachingMetadataReaderFactory extends CachingMetadataReaderFactory {
    private Log logger = LogFactory.getLog(ModuleCachingMetadataReaderFactory.class);

    @Override
    public MetadataReader getMetadataReader(String className) throws IOException {
        List<Module> modules = ModuleManager.getStartedModules();

        logger.debug("Checking if " + className + " is contained in loaded modules");

        for (Module module : modules) {
            if (className.startsWith(module.getPackageName())) {
                String resourcePath = module.getExpandedJarFolder().getAbsolutePath() + "/" + ClassUtils.convertClassNameToResourcePath(className) + ".class";

                File file = new File(resourcePath);

                if (file.exists()) {
                    logger.debug("Yes it is, returning MetadataReader of this class");

                    return getMetadataReader(getResourceLoader().getResource("file:" + resourcePath));
                }
            }
        }

        return super.getMetadataReader(className);
    }
}

并在bean配置中定义:

<bean id="customCachingMetadataReaderFactory" class="com.yourpackage.module.spring.core.type.classreading.ModuleCachingMetadataReaderFactory"/>

<bean name="org.springframework.context.annotation.internalConfigurationAnnotationProcessor"
      class="org.springframework.context.annotation.ConfigurationClassPostProcessor">
      <property name="metadataReaderFactory" ref="customCachingMetadataReaderFactory"/>
</bean>

第 5 步:创建自定义根类加载器、模块类加载器和模块管理器

这是我不会发布课程的部分。所有类加载器都扩展URLClassLoader。

根类加载器

我是单身,所以它可以:

  • 自行初始化
  • 销毁
  • loadClass(模块类、父类、自身类)

最重要的部分是loadClass,它允许上下文在使用setCurrentClassLoader(XmlWebApplicationContext) 之后加载您的模块类(参见下一步的底部)。具体来说,此方法将扫描子类加载器(我个人将其存储在模块管理器中),如果未找到,它将扫描父/自类。

模块类加载器

这个类加载器只是将 module.jar 和它包含的 .jar 添加为 url。

模块管理器

这个类可以加载/启动/停止/卸载你的模块。我确实喜欢这样:

  • load : 存储一个Module 类,代表module.jar(包含id、名称、描述、文件...)
  • 开始:展开jar,创建模块类加载器并将其分配给Module类
  • stop : 移除扩展的 jar,dispose 类加载器
  • 卸载:处理Module类

第 6 步:定义一个有助于进行上下文刷新的类

我将这个类命名为WebApplicationUtils。它包含对调度程序 servlet 的引用(参见步骤 7)。如您所见,refreshContext 调用 AppClassLoader 上的方法,这实际上是我的根类加载器。

/**
 * Refresh {@link DispatcherServlet}
 * @return true if refreshed, false if not
 * @throws RuntimeException
 */
private static boolean refreshDispatcherServlet() throws RuntimeException {
    if (dispatcherServlet != null) {
        dispatcherServlet.refresh();
        return true;
    }

    return false;
}

/**
 * Refresh the given {@link XmlWebApplicationContext}.<br>
 * Call {@link Module#onStarted()} after context refreshed.<br>
 * Unload started modules on {@link RuntimeException}.
 * @param context Application context
 * @param startedModules Started modules
 * @throws RuntimeException
 */
public static void refreshContext(XmlWebApplicationContext context, Module[] startedModules) throws RuntimeException {
    try {
        logger.debug("Closing web application context");
        context.stop();
        context.close();

        AppClassLoader.destroyInstance();

        setCurrentClassLoader(context);

        logger.debug("Refreshing web application context");
        context.refresh();

        setCurrentClassLoader(context);

        AppClassLoader.setThreadsToNewClassLoader();

        refreshDispatcherServlet();

        if (startedModules != null) {
            for (Module module : startedModules) {
                module.onStarted();
            }
        }
    }
    catch (RuntimeException e) {
        for (Module module : startedModules) {
            try {
                ModuleManager.stopModule(module.getId());
            }
            catch (IOException e2) {
                e.printStackTrace();
            }
        }

        throw e;
    }
}

/**
 * Set the current classloader to the {@link XmlWebApplicationContext} and {@link Thread#currentThread()}.
 * @param context ApplicationContext
 */
public static void setCurrentClassLoader(XmlWebApplicationContext context) {
    context.setClassLoader(AppClassLoader.getInstance());
    Thread.currentThread().setContextClassLoader(AppClassLoader.getInstance());
}

第 7 步:定义自定义上下文加载器侦听器

/**
 * Initialize/destroy ModuleManager on context init/destroy
 * @see {@link ContextLoaderListener}
 * @author Ludovic Guillaume
 */
public class ModuleContextLoaderListener extends ContextLoaderListener {
    public ModuleContextLoaderListener() {
        super();
    }

    @Override
    public void contextInitialized(ServletContextEvent event) {
        // initialize ModuleManager, which will scan the given folder
        // TODO: param in web.xml
        ModuleManager.init(event.getServletContext().getRealPath("WEB-INF"), "/dev/temp/modules");

        super.contextInitialized(event);
    }

    @Override
    protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
        XmlWebApplicationContext context = (XmlWebApplicationContext)super.createWebApplicationContext(sc);

        // set the current classloader
        WebApplicationUtils.setCurrentClassLoader(context);

        return context;
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        super.contextDestroyed(event);

        // destroy ModuleManager, dispose every module classloaders
        ModuleManager.destroy();
    }
}

web.xml

<listener>
    <listener-class>com.yourpackage.module.spring.context.ModuleContextLoaderListener</listener-class>
</listener>

第 8 步:定义自定义调度程序 servlet

/**
 * Only used to keep the {@link DispatcherServlet} easily accessible by {@link WebApplicationUtils}.
 * @author Ludovic Guillaume
 */
public class ModuleDispatcherServlet extends DispatcherServlet {
    private static final long serialVersionUID = 1L;

    public ModuleDispatcherServlet() {
        WebApplicationUtils.setDispatcherServlet(this);
    }
}

web.xml

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>com.yourpackage.module.spring.web.servlet.ModuleDispatcherServlet</servlet-class>

    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
    </init-param>

    <load-on-startup>1</load-on-startup>
</servlet>

第 9 步:定义自定义 Jstl 视图

这部分是“可选的”,但它在控制器实现中带来了一些清晰和简洁。

/**
 * Used to handle module {@link ModelAndView}.<br/><br/>
 * <b>Usage:</b><br/>{@code new ModuleAndView("module:MODULE_NAME.jar:LOCATION");}<br/><br/>
 * <b>Example:</b><br/>{@code new ModuleAndView("module:test-module.jar:views/testModule");}
 * @see JstlView
 * @author Ludovic Guillaume
 */
public class ModuleJstlView extends JstlView {
    @Override
    protected String prepareForRendering(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String beanName = getBeanName();

        // checks if it starts 
        if (beanName.startsWith("module:")) {
            String[] values = beanName.split(":");

            String location = String.format("/%s%s/WEB-INF/%s", ModuleManager.CONTEXT_ROOT_MODULES_FOLDER, values[1], values[2]);

            setUrl(getUrl().replaceAll(beanName, location));
        }

        return super.prepareForRendering(request, response);
    }
}

在 bean 配置中定义它:

<bean id="viewResolver"
      class="org.springframework.web.servlet.view.InternalResourceViewResolver"
      p:viewClass="com.yourpackage.module.spring.web.servlet.view.ModuleJstlView"
      p:prefix="/WEB-INF/"
      p:suffix=".jsp"/>

最后一步

现在您只需要创建一个模块,将其与ModuleManager 连接并在WEB-INF/ 文件夹中添加资源。

之后,您可以调用 load/start/stop/unload。除了加载之外,我个人在每次操作后都会刷新上下文。

代码可能是可优化的(例如ModuleManager 作为单例),并且可能有更好的选择(虽然我没有找到)。

我的下一个目标是扫描一个不应该那么困难的模块上下文配置。

【讨论】:

  • 干得好!很好的指南。
  • 谢谢!我花了一段时间,但它的工作就像一个魅力。我将项目迁移到 Wildfly 8.1 并在几天前将 Spring 从 3 更新到 4。它仍在按预期工作:)
  • 该项目仍在 Wildfly 9.0.1 / Spring 4.2.0 上运行。我离开了 Jstl 部分,因为我们不再使用 JSP。我将 XML 配置迁移到 Java 配置。如果我有时间,我会用这一切做一个 git。
  • ~4 年后,我在项目中将此部分标记为已弃用,因为我们正在将模块迁移到 Spring Boot 微服务。
猜你喜欢
  • 2014-01-25
  • 1970-01-01
  • 2014-02-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-28
相关资源
最近更新 更多