gpt4 book ai didi

java - 基于模块化Spring的应用程序

转载 作者:IT老高 更新时间:2023-10-28 13:49:48 25 4
gpt4 key购买 nike

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

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

  • Spring Controller
  • 服务,ejbs ...
  • 资源(jsps,css,图像,javascripts ...)

    因此,当用户添加模块时,应用程序必须按预期注册 Controller ,服务,ejb和映射资源。当他删除时,应用程序将其卸载。

    说起来容易。实际上似乎很难完成。

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

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

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

    我使用什么:
  • Glassfish 3.1.2
  • Spring MVC 3.1.3
  • Spring Security 3.1.3

  • 谢谢。

    编辑:

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

    添加模块:
  • 上传module.jar
  • 处理文件,在模块文件夹
  • 中展开
  • 关闭Spring应用程序上下文
  • 在父类为WebappClassLoader的自定义类加载器中加载JAR
  • 在主项目中复制资源(也许可以找到替代方法,我希望是目前可行的方法)
  • 刷新Spring应用程序上下文

  • 卸下模块:
  • 关闭Spring应用程序上下文
  • 取消绑定(bind)自定义类加载器,并使其进入GC
  • 删除资源
  • 如果保留
  • ,请从模块文件夹+ jar中删除文件
  • 刷新Spring应用程序上下文

  • 对于每个,Spring必须扫描另一个文件夹,而不是
    domains/domain1/project/WEB-INF/classes
    domains/domain1/project/WEB-INF/lib
    domains/domain1/lib/classes

    这实际上是 我当前的发行版

    从技术上讲,我发现涉及到 PathMatchingResourcePatternResolverClassPathScanningCandidateComponentProvider。现在,我需要告诉他们扫描特定的文件夹/类。

    对于其余的内容,我已经进行了一些测试,并且应该可以正常工作。

    有一点是不可能的: jar 里的ejbs。

    完成可用的工作后,我将发布一些资源。

    最佳答案

    好的,我做到了,但是我真的有太多资料可以在这里发布。我将逐步解释我的工作方式,但不会发布对一般熟练的开发人员来说很简单的classloading部分。

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

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

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

  • 因此,对于GF3,classpath中唯一可用的路径是:WEB-INF/classes,WEB-INF/lib ...如果在应用程序服务器上找到一种实现方法,则可以跳过前4个步骤。

    我知道这对于Tomcat是可能的。

    步骤1:创建自定义 namespace 处理程序

    使用其XSD,spring.handlers和spring.schemas创建一个自定义 NamespaceHandlerSupport。该 namespace 处理程序将包含 <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 处理程序
    http\://www.yourwebsite.com/schema/context=com.yourpackage.module.spring.context.config.ModuleContextNamespaceHandler

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

    N.B .: 由于某些问题(例如项目名称)需要使用大于“S”的字母,因此我没有覆盖Spring默认的 namespace 处理程序。我想避免这种情况,所以我创建了自己的 namespace 。

    第2步:创建解析器

    这将由上面创建的 namespace 处理程序初始化。
    /**
    * 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:/<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;
    }
    }

    步骤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.jar的Module类(包含id,名称,描述,文件...)
  • start:扩展jar,创建模块类加载器,并将其分配给Module
  • stop:删除扩展的jar,处置类加载器
  • 卸载:处置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 View

    这部分是“可选的”,但它在 Controller 的实现中带来了一定的清晰度和简洁性。
    /**
    * 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接口(interface),并在WEB-INF/文件夹中添加资源。

    之后,您可以调用加载/启动/停止/卸载。除加载外,我个人在每次操作后刷新上下文。

    该代码可能是可优化的(例如, ModuleManager为单例),也许还有更好的选择(尽管我没有找到它)。

    我的下一个目标是扫描模块上下文配置,这并不难。

    关于java - 基于模块化Spring的应用程序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20242912/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com