简体   繁体   English

基于模块化Spring的应用程序

[英]Modular Spring-based application

I'd like to allow users to add/refresh/update/remove modules in the main project without the need of restart or redeploy. 我想允许用户在主项目中添加/刷新/更新/删除模块,而无需重新启动或重新部署。 Users will be able to code their own modules and add them in the main project. 用户将能够编写自己的模块并将其添加到主项目中。

Technicaly, a module will be a JAR which may be "hot-started" and may contain : 技术上,一个模块将是一个可以“热启动”的JAR,可能包含:

  • spring controllers 弹簧控制器
  • services, ejbs... 服务,ejbs ......
  • resources (jsps, css, images, javascripts...) 资源(jsps,css,图像,javascripts ......)

So, when the user adds a module, the application have to register controllers, services, ejbs and map resources as intend. 因此,当用户添加模块时,应用程序必须按照意图注册控制器,服务,ejbs和映射资源。 When he removes, the application unloads them. 当他删除时,应用程序卸载它们。

Easy to say. 说起来容易。 Actually seems a lot more difficult to do. 实际上似乎要困难得多。

Currently, I did it using Servlet 3.0 and web-fragment.xml . 目前, 我使用Servlet 3.0和web-fragment.xml完成​​了它 The main issue is that I have to redeploy everytime I update a module. 主要问题是我每次更新模块时都必须重新部署。 I need to avoid that. 我需要避免这种情况。

I read some docs about OSGi but I don't understand how I can link it with my project neither how It can load/unload on demand. 我阅读了一些关于OSGi的文档,但我不明白我如何将它与我的项目链接,也不知道它如何按需加载/卸载。

Can someone lead me to a solution or an idea? 有人可以引导我找到解决方案或想法吗?

What I use : 我用的是什么:

  • Glassfish 3.1.2 Glassfish 3.1.2
  • Spring MVC 3.1.3 Spring MVC 3.1.3
  • Spring Security 3.1.3 Spring Security 3.1.3

Thanks. 谢谢。


EDIT: 编辑:

I can now say that it is possible. 我现在可以说这是可能的。 Here's the way I will do : 这是我要做的方式:

Add module : 添加模块:

  1. Upload the module.jar 上传module.jar
  2. Handle the file, expand in a module folder 处理文件,在模块文件夹中展开
  3. Close Spring application context 关闭Spring应用程序上下文
  4. Load JAR in a custom classloader where parent is WebappClassLoader 在自定义类加载器中加载JAR,其中父级是WebappClassLoader
  5. Copy resources in the main project (maybe it will be possible to find alternative, I hope but currently, this should work) 复制主项目中的资源(也许有可能找到替代方案,我希望,但目前,这应该工作)
  6. Refresh Spring application context 刷新Spring应用程序上下文

Remove module : 删除模块:

  1. Close Spring application context 关闭Spring应用程序上下文
  2. Unbind custom classloader and let it go to GC 取消绑定自定义类加载器并将其转到GC
  3. Remove resources 删除资源
  4. Remove files from the module folder + jar if kept 如果保留,则从模块文件夹+ jar中删除文件
  5. Refresh Spring application context 刷新Spring应用程序上下文

For each, Spring have to scan another folder than 对于每个,Spring必须扫描另一个文件夹而不是

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

And that's actually my current issue . 这实际上是我目前的问题

Technicaly, I found PathMatchingResourcePatternResolver and ClassPathScanningCandidateComponentProvider was involved. 技术上,我发现了PathMatchingResourcePatternResolverClassPathScanningCandidateComponentProvider Now I need to tell them to scan specific folder/classes. 现在我需要告诉他们扫描特定的文件夹/类。

For the rest, I already did some tests and it should work as intended. 其余的,我已经做了一些测试,它应该按预期工作。

One point which will not be possible : ejbs in the jar. 有一点是不可能的:罐子里的ejbs。

I'll post some sources when I'd have done something usable. 当我做了一些可用的事情时,我会发布一些消息来源。

Ok, I did it but I have really too much sources to post it here. 好的,我做到了,但我有太多的消息来源在这里发布。 I will explain step by step how I did but won't post the classloading part which is simple for an average skilled developper. 我将逐步解释我是如何做的,但不会发布对于普通熟练的开发人员来说简单的类加载部分。

One thing is currently not supported by my code is the context config scan. 我的代码目前不支持的一件事是上下文配置扫描。

First, the explanation below depends on your needs and also your application server. 首先,下面的解释取决于您的需求以及您的应用程序服务器。 I use Glassfish 3.1.2 and I did not find how to configure a custom classpath : 我使用Glassfish 3.1.2,但我没有找到如何配置自定义类路径:

  • classpath prefix/suffix not supported anymore 不再支持classpath前缀/后缀
  • -classpath parameter on the domain's java-config did not work 域的java-config上的-classpath参数不起作用
  • CLASSPATH environment did not work either CLASSPATH环境也不起作用

So the only available paths in classpath for GF3 are : WEB-INF/classes , WEB-INF/lib ... If you find a way to do it on your application server, you can skip the first 4 steps. 因此,GF3的类路径中唯一可用的路径是: WEB-INF / classesWEB-INF / lib ...如果您在应用程序服务器上找到了一种方法,可以跳过前4个步骤。

I know this is possible with Tomcat. 我知道Tomcat可以实现这一点。

Step 1 : Create a custom namespace handler 第1步:创建自定义命名空间处理程序

Create a custom NamespaceHandlerSupport with its XSD, spring.handlers and spring.schemas . 使用其XSD, spring.handlersspring.schemas创建自定义NamespaceHandlerSupport This namespace handler will contain a redefinition of <context:component-scan/> . 此命名空间处理程序将包含<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());
    }
}

The XSD contains only component-scan element which is a perfect copy of the Spring's one. XSD只包含component-scan元素,它是Spring的完美副本。

spring.handlers spring.handlers

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

spring.schemas spring.schemas

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

NB: I didn't override the Spring default namespace handler due to some issues like the name of the project which need to have a letter greater than 'S'. 注意:我没有覆盖Spring默认的命名空间处理程序,因为一些问题,比如需要字母大于'S'的项目名称。 I wanted to avoid that so I made my own namespace. 我想避免这种情况,所以我创建了自己的命名空间。

Step 2 : Create the parser 第2步:创建解析器

This will be initialized by the namespace handler created above. 这将由上面创建的命名空间处理程序初始化。

/**
 * 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);
    }
}

Step 3 : Create the scanner 第3步:创建扫描仪

Here's the custom scanner which uses the same code as ClassPathBeanDefinitionScanner . 这是自定义扫描程序,它使用与ClassPathBeanDefinitionScanner相同的代码。 The only code changed is String packageSearchPath = "file:" + ModuleManager.getExpandedModulesFolder() + "/**/*.class"; 唯一更改的代码是String packageSearchPath = "file:" + ModuleManager.getExpandedModulesFolder() + "/**/*.class"; .

ModuleManager.getExpandedModulesFolder() contains an absolute url. ModuleManager.getExpandedModulesFolder()包含绝对URL。 eg: C:/<project>/modules/ . 例如: C:/<project>/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;
    }
}

Step 4 : Create a custom resource caching implementation 第4步:创建自定义资源缓存实现

This will allow Spring to resolve your module classes out of the classpath. 这将允许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);
    }
}

And define it in the bean configuration : 并在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>

Step 5 : Create a custom root classloader, module classloader and module manager 步骤5:创建自定义根类加载器,模块类加载器和模块管理器

This is the part I won't post classes. 这是我不会发布课程的部分。 All classloaders extend URLClassLoader . 所有类加载器都扩展了URLClassLoader

Root classloader 根类加载器

I did mine as singleton so it can : 我做了我的单身,所以它可以:

  • initialize itself 初始化自己
  • destroy 破坏
  • loadClass (modules classes, parent classes, self classes) loadClass(模块类,父类,自习类)

The most important part is loadClass which will allow context to load your modules classes after using setCurrentClassLoader(XmlWebApplicationContext) (see bottom of the next step). 最重要的部分是loadClass ,它允许上下文在使用setCurrentClassLoader(XmlWebApplicationContext)后加载模块类(参见下一步的底部)。 Concretly, this method will scan the children classloader (which I personaly store in my module manager) and if not found, it will scan parent/self classes. 具体来说,这个方法将扫描子类加载器(我个人存储在我的模块管理器中),如果没有找到,它将扫描父类/自我类。

Module classloader 模块类加载器

This classloader simply adds the module.jar and the .jar it contains as url. 这个类加载器只是将module.jar和它包含的.jar添加为url。

Module manager 模块经理

This class can load/start/stop/unload your modules. 该类可以加载/启动/停止/卸载您的模块。 I did like this : 我喜欢这样:

  • load : store a Module class which represent the module.jar (contains id, name, description, file...) load:存储一个Module类,代表module.jar (包含id,name,description,file ......)
  • start : expand the jar, create module classloader and assign it to the Module class start:展开jar,创建模块类加载器并将其分配给Module
  • stop : remove the expanded jar, dispose classloader 停止:删除展开的jar,配置classloader
  • unload : dispose Module class 卸载:dispose Module

Step 6 : Define a class which will help to do context refreshs 第6步:定义一个有助于进行上下文刷新的类

I named this class WebApplicationUtils . 我将此类命名为WebApplicationUtils It contains a reference to the dispatcher servlet (see step 7). 它包含对调度程序servlet的引用(请参阅步骤7)。 As you will see, refreshContext call methods on AppClassLoader which is actually my root classloader. 正如您将看到的,在AppClassLoader上的refreshContext调用方法实际上是我的根类加载器。

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

Step 7 : Define a custom context loader listener 第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 web.xml中

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

Step 8 : Define a custom dispatcher servlet 第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 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>

Step 9 : Define a custom Jstl view 第9步:定义自定义Jstl视图

This part is 'optional' but it brings some clarity and cleanness in the controller implementation. 这部分是“可选的”,但它在控制器实现中带来了一些清晰度和清晰度。

/**
 * 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);
    }
}

Define it in the bean config : 在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"/>

Final step 最后一步

Now you just need to create a module, interface it with ModuleManager and add resources in the WEB-INF/ folder. 现在您只需创建一个模块,将其与ModuleManager并在WEB-INF /文件夹中添加资源。

After that you can call load/start/stop/unload. 之后,您可以调用load / start / stop / unload。 I personaly refresh the context after each operation except for load. 除了加载之外,我每次操作后都会刷新上下文。

The code is probably optimizable ( ModuleManager as singleton eg) and there's maybe a better alternative (though I did not find it). 代码可能是可优化的(例如, ModuleManager作为单例)并且可能有更好的替代方案(尽管我没有找到它)。

My next goal is to scan a module context config which shouldn't be so difficult. 我的下一个目标是扫描模块上下文配置,这应该不是那么困难。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 由于配置错误,无法加载基于Spring的应用程序 - Spring-based Application not loading due to misconfiguration 在基于Spring的应用程序中不推荐使用AbstractExcelView - AbstractExcelView is deprecated in Spring-based application 是否可以在基于控制台的基于Spring的应用程序中使用@Valid注释? - Is it possible to use @Valid annotation in console Spring-based application? 为基于 spring 的 Web 应用程序中的每个请求分配一个唯一的 id - Assign a unique id to every request in a spring-based web application 基于Spring的Web应用程序的特定于环境的配置? - Environment-specific configuration for a Spring-based web application? 将基于Spring的Java模块集成到独立的Java应用程序中 - Integrate Spring-based java module into a standalone java application EJB 中的 Facades 与基于 Spring 的 Web 应用程序中的服务是否相同 - Are Facades in EJB the same thing as service in a spring-based web application 如何在基于Spring的Web应用程序中发现性能瓶颈 - How to find a performance bottleneck in a Spring-Based Web Application “面向聚合的域”是什么意思(说到基于 Spring 的应用程序)? - What "aggregate-oriented domain" means (speaking of a Spring-based application)? 使用Spring的模块化Web应用程序 - Modular web application with Spring
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM