# JModuleLink
**Repository Path**: zhangpeng_jpa/JModuleLink
## Basic Information
- **Project Name**: JModuleLink
- **Description**: 动态模块,实现模块热部署
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 6
- **Created**: 2021-06-25
- **Last Updated**: 2021-06-25
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 第一部分 简介
`JModuleLink`是一个基于`JAVA`的模块化开发框架,它提供在运行时动态加载模块(一个或一组JAR包)、卸载模块的API,使开发者更加关注业务本身。
## 1.1 需求背景
现在在公司主要从事支付平台的开发工作,随着业务的增长,对接的银行支付通道越来越多,用户也不满足在一个通道下面开通单个商户,存在一个银行通道按照不同商户进行支付的需求,在传统实现当中,每次涉及到银行通道或者商户号的变更都需要重启服务器,体验较差。单个通道出现异常会影响其他通道的正常使用,用户对商户号的修改需要依赖开发人员对系统进行配置,增大后期维护成本。在同一个支付通道的多商户的背景下,部门银行提供的开发包无法兼容,需要对其进行隔离,于是在系统中使用了自定义的`ClassLoader`突破默认的双亲委托,自定义加载所需三方包以解决 冲突的问题,偶然的机会看到了阿里开源的`jarslink`,所以借鉴了其部分实现方式,编写了`JModuleLink`。
传统开发模式问题:
- 应用拆分的多或少都有问题。多则维护成本高,每次发布一堆应用。少则拆分成本高,无用功能很难下线。
- 故障不隔离。当一个系统由多人同时参与开发时,修改A功能,可能会影响B功能,引发故障。
- 多分支开发引发冲突。多分支开发完之后合并会产生冲突。
- 牵一发动全身。一处核心代码的改动,或一个基础Jar的升级需要回归整个系统。
- 升级和迁移成本高。中间件升级每个应用都有升级成本。
## 1.2 模块化开发的好处
- 可插拔,一个应用由多个模块组成,应用里的模块可拆和合,模块可快速在多个系统中迁移和部署。
- 模块化开发,模块之间互相隔离,实现故障隔离。
- 一个模块一个分支,不会引发代码冲突。
- 在模块中增加或修改功能,只会影响当前模块,不会影响整个应用。
- 动态部署,在运行时把模块部署到应用中,快速修复故障,提高发布效率。
- 多版本部署,可以在运行时同时部署某个模块的新旧版本,进行AB TEST。
- 减少资源消耗,通过部署模块的方式减少应用数量和机器数量。
## 1.3 特性
### 1.3.1 隔离性
- 类隔离:框架为每个模块的`Class`使用单独的`ClassLoader`来加载,每个模块可以依赖同一种框架的不同的版本。
- 实例隔离:框架为每个模块创建了一个独立的`Spring`上下文,来加载模块中的BEAN,实例化失败不会影响其他模块(Spring环境)。
### 1.3.2 动态性
- 动态发布:模块能在运行时动态加载到系统中,实现不需要重启和发布系统新增功能。支持突破双亲委派机制,在运行时加载父加载器已经加载过的类,实现模块升级依赖包不需要系统发布。
- 动态卸载:模块能在运行时被动态卸载干净,实现快速下线不需要功能。
### 1.3.3 易用性
- 提供了通用灵活的API让系统和模块进行交互。
# 第二部分 快速开始
使用`JModuleLink`可以直接下载源代码编译或者下载已经编译的`jar`文件,如果您是使用`maven`来构建项目,也可以直接在`pom.xml`中添加`JModuleLink`的坐标:
[](https://maven-badges.herokuapp.com/maven-central/com.jianggujin/JModuleLink)
```xml
com.jianggujin
JModuleLink
最新版本
```
最新的版本可以从[Maven仓库](http://mvnrepository.com/artifact/com.jianggujin/JModuleLink)或者[码云](https://gitee.com/jianggujin)获取。
## 2.1 编写模块
对于模块而言,简单的来说就是`Action`的合集,所以我们需要掌握如何开发`Action`,编写一个`Action`我们只需要创建一个普通的类,然后让其实现`JAction`接口即可。
```java
package com.jianggujin.modulelink.test.module;
import com.jianggujin.modulelink.JAction;
import com.jianggujin.modulelink.exception.JModuleLinkException;
/**
* 自定义Action
*
* @author jianggujin
*
*/
public class CustomAction implements JAction {
@Override
public Object execute(JModuleContext context) throws JModuleLinkException {
// TODO
return null;
}
@Override
public String getActionName() {
return "customAction";
}
@Override
public boolean isDefault(String moduleName) {
return false;
}
}
```
在`JAction`中包含三个方法:
| 修饰符与类型 | 方法 | 描述 |
| :----------: | :-----------------------------: | :----------------------------------------------------------: |
| Object | execute(JModuleContext context) | 真正的业务逻辑处理方法 |
| String | getActionName() | `Action`的名称,在同一个模块中,该值唯一 |
| boolean | isDefault(String moduleName) | 该方法用于判断`Action`在模块中
是否为默认的`Action`,
用于找不到指定`Action`的时候
的默认处理(可控制是否使用) |
最后只需要将编写好的模块打包即可。建议一个模块就是一个`FAT JAR`,当然了,这并不是强制的,`JModuleLink`也支持一个模块有多个资源或`jar`。除了上面的写法之外,`JModuleLink`提供了一个`JAction`的抽象实现`JAbstractAction`,多数情况下,我们只需要重写`execute`方法即可,默认的`Action`名称为当前类名首字母小写。
```java
package com.jianggujin.modulelink.test.module;
import com.jianggujin.modulelink.exception.JModuleLinkException;
import com.jianggujin.modulelink.support.JAbstractAction;
/**
* 自定义Action
*
* @author jianggujin
*
*/
public class CustomAction extends JAbstractAction {
@Override
public Object execute(JModuleContext context) throws JModuleLinkException {
// TODO
return null;
}
}
```
## 2.2 模块管理器
模块的加载运行需要`JModuleLink`的核心支持,首先我们需要初始化`JModuleManager`,使用该类可以完成模块的加载、卸载、查找等操作,建议使用单例模式初始化该类的对象,使用全局唯一的模块管理器管理模块。
```java
JModuleManager moduleManager = new JDefaultModuleManager();
```
除此之外,还可以使用更加简单的方式来获得`JModuleManager`的实例对象:
```java
JModuleManager moduleManager = JDefaultModuleManager.getInstance();
```
该种方式获得的`JModuleManager`对象为单例对象,且只有第一次调用该方法的时候才会真正初始化。
## 2.3 加载模块
我们可以使用`JModuleConfig `来定义一个模块信息。配置说明如下:
| 名称 | 说明 |
| :----------------------: | :----------------------------------------------------------: |
| moduleName | 模块名,建议用英文命名,全局唯一 |
| description | 模块描述 |
| moduleUrls | 模块的资源地址,比如jar文件的位置 |
| exclusionActions | 需要排除的action,值为全限定类名 |
| scanPackages | 需要扫描的包,不为空时会启动扫描包下面的`Action`、监听器、拦截器。
如果使用Spring配置且模块环境为Spring,该值为空时,尝试使用XML加载,否则使用注解。
在SpringBoot模块环境下该值无效 |
| reload | 重新加载,如果该值为`true`,加载模块时会先卸载已加载模块然后重新加载 |
| exclusionModuleListeners | 需要排除的模块事件监听器 |
| exclusionInterceptors | 需要排除的拦截器 |
使用示例:
```java
JModuleConfig moduleConfig = new JModuleConfig(moduleName);
moduleConfig.setDescription(moduleDesc);
moduleConfig.addModuleUrl(moduleUrl);
moduleConfig.addScanPackage(scanPackage);
moduleManager.load(moduleConfig);
```
这样就完成了一个模块的加载。
## 2.4 使用模块
模块正常加载后,我们就可以通过模块管理器查找已经加载的模块并执行指定的`Action`。使用方式如下:
```java
public Object service(String moduleName, String actionName) {
JModule module = moduleManager.getModule(moduleName);
if (actionName == null) {
return module.doDefaultAction(null);
}
return module.doAction(actionName, null);
}
```
> `Action`的参数按照实际情况传递
## 2.5 卸载模块
当一个模块不再使用的时候,我们可以动态的将其卸载,`JModuleManager`提供了相应的卸载方法:
```java
/**
* 卸载一个模块
*/
void unload(String moduleName);
```
> 卸载模块的时候必须调用`JModuleManager`的`unload`方法实现,虽然该接口提供了查找`JModule`的方法,并且`JModule`存在`destroy()`方法,但是我们依然不建议直接调用模块的的销毁方法,避免管理器模块状态不一致出现错误。
具体示例请参考:[https://gitee.com/jianggujin/JModuleLink-demo](https://gitee.com/jianggujin/JModuleLink-demo)
# 第三部分 扩展阅读
## 3.1 事件监听
在`JModuleLink`中,我们可以对模块的开始加载、加载完成、激活、取消激活、卸载事件进行监听,以便于我们在代码中做一些额外的处理,监听模块的事件,我们需要实现`JModuleListener`接口,模块加载过程中,会先尝试从模块中加载实现了该接口的监听类,然后才会加载相关的`Action`,以便于监听器可以完整的获得事件回调。如果只需要监听模块的部分事件,`JModuleLink`中还提供了一个`JModuleListenerAdapter`,我们只需要重写需要处理的回调方法即可。
## 3.2 Spring模块
`JModuleLink`支持`Spring`模块,默认提供的加载器会针对`Spring`环境的模块进行特殊处理,我们可以使用`XML`或注解的形式创建`Spring`环境的上下文,当然,上下文的创建是加载器根据模块配置进行选择的。`JModuleLink`为`Spring`模块提供专用的配置类`JSpringModuleConfig`,该配置中除了拥有`JModuleConfig`的所有属性之外,还拥有一些扩展属性,扩展属性如下:
| 名称 | 说明 |
| :---------: | :----------------------------------------------------------: |
| xmlPatterns | XML配置,默认加载的配置文件:`classpath*:META-INF/spring/*.xml`、`classpath*:*META-INF/spring/*.xml`,自定义配置文件后,默认配置无效 |
| exclusions | 需要排除的配置文件 |
需要注意的是,在模块配置中,如果配置了`scanPackages`,则模块加载器会初始化`AnnotationConfigApplicationContext`作为上下文,此时`xmlPatterns`与`exclusions`配置无效,否则使用`ClassPathXmlApplicationContext`初始化上下文,加载器会扫描上下文中注入的`Action`和`监听器`,需要排除的`Action`和`监听器`的配置是正常生效的,排除的`Action`只能作为普通`Bean`使用而不能被模块执行。
想要加载器将模块作为`Spring`环境加载需要满足以下条件:
- 配置类使用`JSpringModuleConfig`或`JSpringBootModuleConfig`
- 模块类路径中可以加载到`org.springframework.context.support.ClassPathXmlApplicationContext`对象,这里需要分清楚的是,`JModuleLink`的运行环境不一定是`Spring`环境也可以正常加载作为`Spring`环境的模块,主要在模块类路径下或`JModuleLink`运行的类路径下扫描到`Spring`相关类即可
如果加载器检测模块环境以及配置不满足`Spring`的加载条件,则会将其作为普通模块加载处理。
## 3.3 SpringBoot模块
与`Spring`环境的模块类似,`JModuleLinke`也专门提供了对`SpringBoot`环境模块的特殊处理,使用`SpringBoot`才需要使用`JModuleSpringBootConfig`作为配置类,该配置中除了拥有`JModuleConfig`的所有属性之外,还拥有一些扩展属性,扩展属性如下:
| 名称 | 说明 |
| :-----: | :---------------: |
| sources | SpringBoot source |
在`SpringBoot`环境下,`scanPackages`配置无效,将会将扫描与初始化完全交给`SpringBoot`处理,与`Spring`环境类似,想要加载器将模块作为`SpringBoot`环境加载需要满足以下条件:
- 配置类使用`JSpringBootModuleConfig`
- 模块类路径中可以加载到`org.springframework.boot.SpringApplication`对象
如果加载器检测模块环境以及配置不满足`SpringBoot`的加载条件,则会将其作为普通模块加载处理。
## 3.4 Web环境
`JModuleLink`的初衷是实现可插拔、多模块的热部署,对于`Web`环境暂时没有做过多的支持,后期可能会增加直接初始化`Web`环境模块的增强处理,目前针对`Web`环境做了简单的适配,提供了`JGenericHttpAction`和`JHttpAction`两个用于`Web`环境的`Action`,这两个实现的关系和`HttpServlet`与`GenericServlet`非常相似,可以按照`Servlet`的开发方式开发`Action`。在执行`Action`的时候我们只需要使用`JHttpHolder`将`HttpServletRequest`与`HttpServletResponse`进行包装并作为入参传递,就可以使其正常工作。