Java 基于Spring的模块化应用
我希望允许用户在主项目中添加/刷新/更新/删除模块,而无需重新启动或重新部署。用户将能够编写自己的模块并将其添加到主项目中 从技术上讲,模块将是一个JAR,它可能是“热启动”的,并且可能包含:Java 基于Spring的模块化应用,java,spring,jsp,spring-mvc,Java,Spring,Jsp,Spring Mvc,我希望允许用户在主项目中添加/刷新/更新/删除模块,而无需重新启动或重新部署。用户将能够编写自己的模块并将其添加到主项目中 从技术上讲,模块将是一个JAR,它可能是“热启动”的,并且可能包含: 弹簧控制器 服务,EJB 资源(JSP、css、图像、javascripts…) 因此,当用户添加模块时,应用程序必须按照预期注册控制器、服务、EJB和映射资源。当他删除时,应用程序将卸载它们 说起来容易。实际上似乎要困难得多 目前。主要问题是每次更新模块时都必须重新部署。我需要避免这种情况 我读了一
- 弹簧控制器
- 服务,EJB
- 资源(JSP、css、图像、javascripts…)
- 玻璃鱼3.1.2
- Spring MVC 3.1.3
- Spring Security 3.1.3
编辑: 我现在可以说这是可能的。我会这样做: 添加模块:
domains/domain1/project/WEB-INF/classes
domains/domain1/project/WEB-INF/lib
domains/domain1/lib/classes
这就是我当前的问题
从技术上讲,我发现涉及到路径匹配源模式解析程序和类路径扫描候选组件提供程序。现在我需要告诉他们扫描特定的文件夹/类
对于其余部分,我已经做了一些测试,它应该可以正常工作
有一点是不可能的:jar中的ejb
当我做了一些有用的事情时,我会发布一些资源。好的,我做了,但我有太多的资源,无法在这里发布。我将一步一步地解释我是如何做到的,但不会发布类加载部分,这对于一般熟练的开发人员来说很简单
我的代码目前不支持的一件事是上下文配置扫描
首先,下面的解释取决于您的需要以及您的应用服务器。我使用Glassfish 3.1.2,但没有找到如何配置自定义类路径:
- 不再支持类路径前缀/后缀
-classpath
域的java配置上的参数不起作用
- 类路径环境也不起作用
因此,GF3类路径中唯一可用的路径是:WEB-INF/classes、WEB-INF/lib。。。如果您找到了在应用服务器上执行此操作的方法,则可以跳过前4个步骤
我知道这在Tomcat身上是可能的
步骤1:创建自定义命名空间处理程序
使用XSD、spring.handlers和spring.schema创建一个自定义的NamespaceHandlerSupport
。此命名空间处理程序将包含对
的重新定义
XSD只包含组件扫描
元素,它是Spring的完美副本
弹簧处理器
spring.schemas
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);
}
}
步骤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);
}
}
步骤7:定义自定义上下文加载程序侦听器
web.xml
web.xml
调度员
com.yourpackage.module.spring.web.servlet.ModuleDispatcherServlet
上下文配置位置
/WEB-INF/dispatcher-servlet.xml
1.
步骤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);
}
}
/**
*用于处理模块{@link ModelAndView}。
*用法:
{@code new moduleadview(“module:module_NAME.jar:LOCATION”);}
*示例:
{@code新模块adview(“module:test module.jar:views/testModule”);}
*@see JstlView
*@作者卢多维奇·纪尧姆
*/
公共类ModuleJstlView扩展了JstlView{
@凌驾
受保护的字符串prepareForRendering(HttpServletRequest请求、HttpServletResponse响应)引发异常{
字符串beanName=getBeanName();
//检查它是否启动
if(beanName.startsWith(“模块:”){
字符串[]值=beanName.split(“:”);
String location=String.format(“/%s%s/WEB-INF/%s”,ModuleManager.CONTEXT_ROOT_MODULES_文件夹,值[1],值[2]);
setUrl(getUrl().replaceAll(beanName,location));
}
返回super.prepareForRendering(请求、响应);
}
}
在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),可能还有更好的选择(尽管我没有找到)
我的下一个目标是扫描一个模块上下文配置,其中
/**
* 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;
}
}
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 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>
/**
* 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();
}
}
<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);
}
}
<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>
/**
* 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 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"/>