Модульное приложение Spring

Я хотел бы разрешить пользователям добавлять/обновлять/обновлять/удалять модули в основном проекте без необходимости перезапуска или повторного развертывания. Пользователи смогут создавать собственные модули и добавлять их в основной проект.

Технически, модуль будет JAR, который может быть "горячим" и может содержать:

  • spring контроллеры
  • услуги, ejbs...
  • ресурсы (jsps, css, images, javascripts...)

Итак, когда пользователь добавляет модуль, приложение должно регистрировать контроллеры, службы, ejbs и ресурсы карты как предназначенные. Когда он удаляет, приложение выгружает их.

Легко сказать. На самом деле это гораздо труднее сделать.

В настоящее время я сделал это с помощью Servlet 3.0 и web-fragment.xml. Основная проблема заключается в том, что я должен передислоцировать каждый раз, когда я обновляю модуль. Мне нужно избегать этого.

Я прочитал некоторые документы об OSGi, но я не понимаю, как я могу связать его с моим проектом ни тем, как он может загружать/выгружать по требованию.

Может ли кто-нибудь привести меня к решению или идее?

Что я использую:

  • Glassfish 3.1.2
  • spring MVC 3.1.3
  • spring Безопасность 3.1.3

Спасибо.


EDIT:

Теперь я могу сказать, что это возможно. Вот как я это сделаю:

Добавить модуль:

  • Загрузите module.jar
  • Обращайтесь к файлу, разверните его в папке с модулем
  • Закрыть контекст приложения Spring
  • Загрузите JAR в пользовательский загрузчик классов, где parent - это WebappClassLoader
  • Скопируйте ресурсы в основной проект (возможно, можно будет найти альтернативу, надеюсь, но в настоящее время это должно работать)
  • Обновить контекст приложения Spring

Удалить модуль:

  • Закрыть контекст приложения Spring
  • Отвяжите пользовательский загрузчик классов и отпустите его в GC
  • Удалить ресурсы
  • Удалите файлы из папки модулей + jar, если они сохранены
  • Обновить контекст приложения Spring

Для каждого Spring необходимо сканировать другую папку, чем

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

И это действительно моя текущая проблема.

Технически я нашел PathMatchingResourcePatternResolver и ClassPathScanningCandidateComponentProvider. Теперь мне нужно сказать им, чтобы они сканировали определенные папки/классы.

В остальном я уже провел несколько тестов, и он должен работать по назначению.

Одна точка, которая будет невозможна: ejbs в банке.

Я опубликую некоторые источники, когда бы сделал что-то полезное.

Ответ 1

Хорошо, я сделал это, но у меня действительно слишком много источников, чтобы опубликовать его здесь. Я буду объяснять шаг за шагом, как я это сделал, но не буду публиковать часть, загружающую класс, которая проста для среднего опытного разработчика.

Одна вещь в настоящее время не поддерживается моим кодом - это проверка конфигурации контекста.

Во-первых, объяснение ниже зависит от ваших потребностей, а также от вашего сервера приложений. Я использую Glassfish 3.1.2, и я не нашел, как настроить собственный путь к классам:

  • префикс/суффикс classpath больше не поддерживается
  • -classpath параметр в домене java-config не работает.
  • Среда CLASSPATH не работает.

Таким образом, единственными доступными путями в пути к классам для GF3 являются: WEB-INF/classes, WEB-INF/lib... Если вы найдете способ сделать это на своем сервере приложений, вы можете пропустить первые 4 шага.

Я знаю, что это возможно с Tomcat.

Шаг 1: Создайте собственный обработчик пространства имен

Создайте пользовательский NamespaceHandlerSupport с его XSD, spring.handlers и spring.schemas. Этот обработчик пространства имен будет содержать переопределение <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

N.B.: Я не переопределял обработчик пространства имен 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:/<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: Создайте собственный загрузчик классов root, загрузчик модулей и диспетчер модулей

Это часть, которую я не буду публиковать в классах. Все загрузчики классов расширяют URLClassLoader.

Root classloader

Я сделал свой синглтон, чтобы он мог:

  • инициализировать себя
  • уничтожить
  • loadClass (классы модулей, родительские классы, самоклассы)

Самая важная часть - loadClass, которая позволит контексту загружать классы модулей после использования setCurrentClassLoader(XmlWebApplicationContext) (см. нижнюю часть следующего шага). Конкретно, этот метод будет сканировать дочерний класс loader (который я лично храню в моем диспетчере модулей), и если он не найден, он будет сканировать родительские/собственные классы.

загрузчик классов модулей

Этот загрузчик классов просто добавляет module.jar и .jar, он содержит в качестве url.

Менеджер модулей

Этот класс может загружать/запускать/останавливать/выгружать ваши модули. Мне это понравилось:

  • load: сохраните класс Module, который представляет module.jar(содержит идентификатор, имя, описание, файл...)
  • start: разверните банку, создайте загрузчик классов модулей и назначьте его классу Module
  • stop: удалить расширенную банку, выгрузить classloader
  • unload: dispose Module класс

Шаг 6: Определите класс, который поможет делать обновления контекста

Я назвал этот класс WebApplicationUtils. Он содержит ссылку на сервлет диспетчера (см. Шаг 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: Определите пользовательский сервлет диспетчера

/**
 * 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 как singleton, например), и может быть лучшая альтернатива (хотя я его не нашел).

Моя следующая цель - отсканировать конфигурацию контекста модуля, которая не должна быть настолько сложной.