[英]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,可能包含:
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 : 我用的是什么:
Thanks. 谢谢。
EDIT: 编辑:
I can now say that it is possible. 我现在可以说这是可能的。 Here's the way I will do : 这是我要做的方式:
Add module : 添加模块:
Remove module : 删除模块:
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. 技术上,我发现了PathMatchingResourcePatternResolver
和ClassPathScanningCandidateComponentProvider
。 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
parameter on the domain's java-config did not work 域的java-config上的-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 / classes , WEB-INF / lib ...如果您在应用程序服务器上找到了一种方法,可以跳过前4个步骤。
I know this is possible with Tomcat. 我知道Tomcat可以实现这一点。
Create a custom NamespaceHandlerSupport
with its XSD, spring.handlers and spring.schemas . 使用其XSD, spring.handlers和spring.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. 我想避免这种情况,所以我创建了自己的命名空间。
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);
}
}
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;
}
}
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>
This is the part I won't post classes. 这是我不会发布课程的部分。 All classloaders extend URLClassLoader
. 所有类加载器都扩展了URLClassLoader
。
I did mine as singleton so it can : 我做了我的单身,所以它可以:
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. 具体来说,这个方法将扫描子类加载器(我个人存储在我的模块管理器中),如果没有找到,它将扫描父类/自我类。
This classloader simply adds the module.jar and the .jar it contains as url. 这个类加载器只是将module.jar和它包含的.jar添加为url。
This class can load/start/stop/unload your modules. 该类可以加载/启动/停止/卸载您的模块。 I did like this : 我喜欢这样:
Module
class which represent the module.jar (contains id, name, description, file...) load:存储一个Module
类,代表module.jar (包含id,name,description,file ......) Module
class start:展开jar,创建模块类加载器并将其分配给Module
类 Module
class 卸载:dispose Module
类 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());
}
/**
* 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>
/**
* 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>
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"/>
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.