# JModuleLink **Repository Path**: steven_Jiang/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**: 2019-09-03 - **Last Updated**: 2020-12-19 ## 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`的坐标: [![Maven central](https://maven-badges.herokuapp.com/maven-central/com.jianggujin/JModuleLink/badge.svg)](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.JModuleLinkException; /** * 自定义Action * * @author jianggujin * */ public class CustomAction implements JAction { @Override public Object execute(Object in) throws JModuleLinkException { // TODO return null; } @Override public String getActionName() { return "customAction"; } @Override public boolean isDefault(String moduleName) { return false; } } ``` 在`JAction`中包含三个方法: | 修饰符与类型 | 方法 | 描述 | | :----------: | :--------------------------: | :----------------------------------------------------------: | | O | execute(I in) | 真正的业务逻辑处理方法,
`I`为入参数据类型,
`O`为返回数据类型 | | 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.JModuleLinkException; import com.jianggujin.modulelink.impl.JAbstractAction; /** * 自定义Action * * @author jianggujin * */ public class CustomAction extends JAbstractAction { @Override public Object execute(Object in) throws JModuleLinkException { // TODO return null; } } ``` ## 2.2 初始化 JModuleManager 模块的加载运行需要`JModuleLink`的核心支持,首先我们需要初始化`JModuleManager`,使用该类可以完成模块的加载、卸载、查找等操作,建议使用单例模式初始化该类的对象,使用全局唯一的模块管理器管理模块。 ```java JModuleManager moduleManager = new JModuleManagerImpl(); ``` 除此之外,还可以使用更加简单的方式来获得`JModuleManager`的实例对象: ```java JModuleManager moduleManager = JModuleManagerImpl.getInstance(); ``` 该种方式获得的`JModuleManager`对象为单例对象,且只有第一次调用该方法的时候才会真正初始化。 ## 2.3 加载模块 我们可以使用`JModuleConfig `来定义一个模块信息。配置说明如下: | 名称 | 说明 | | :--------------: | :----------------------------------------------------------: | | name | 模块名,建议用英文命名,全局唯一 | | desc | 模块描述 | | version | 模块版本 | | moduleUrls | 模块的资源地址,比如jar文件的位置 | | actions | 需要使用的action,值为全限定类名 | | exclusionActions | 需要排除的action,值为全限定类名 | | scanPackages | 需要扫描的包,不为空时会启动扫描。默认的实现会扫描包下面的`JAction`实现类;如果使用`spring`, 将自动扫描注解形式的bean ,并装配扫描到的`JAction` | | active | 是否激活,仅当模块有多个版本的时候有效 | | reload | 重新加载,如果该值为`true`,加载模块时会先卸载已加载模块然后重新加载 | 使用示例: ```java JModuleConfig moduleConfig = new JModuleConfig("moduleName", "moduleVersion"); moduleConfig.setDesc(moduleDesc); moduleConfig.addModuleUrl(new File(modulePath).toURI().toURL()); moduleConfig.addScanPackage(scanPackage); moduleManager.load(moduleConfig); ``` 这样就完成了一个模块的加载。 ## 2.4 使用模块 模块正常加载后,我们就可以通过模块管理器查找已经加载的模块并执行指定的`Action`。一种比较推荐的使用方式如下: ```java public Object service(String moduleName, String actionName) { if (moduleManager.has(moduleName)) { JModule module = moduleManager.find(moduleName); if (actionName == null) { if (module.hasDefaultAction()) { return module.doDefaultAction(null); } } else { if (module.hasAction(actionName)) { return module.doAction(actionName, null); } } throw new JModuleNotFoundException("Not found action."); } throw new JModuleNotFoundException("Not found module."); } ``` 建议直接使用`JModule`提供的`doAction`方法执行指定的`Action`,或者使用`JModuleUtils `提供的`doActionWithinModuleClassLoader `方法执行,否则可能会导致模块的类加载器无法正常使用。 > `Action`的参数按照实际情况传递 ## 2.5 AB测试 如果我们希望对一个模块的不同版本进行`AB TEST`,我们只需要加载指定模块的不同版本,然后使用模块管理器的`activeVersion(String name, String version) `可以直接切换模块默认使用的版本。 ## 2.6 模块卸载销毁 当一个模块或模块的版本不再使用的时候,我们可以动态的将其卸载,`JModuleManager`提供了相应的卸载方法: ```java /** * 卸载一个模块 */ void unload(String name); /** * 卸载一个模块的指定版本 */ void unload(String name, String version); ``` 需要注意的是如果仅仅是卸载模块的指定版本,且该版本为当前正在使用的版本,那么被卸载的模块中除去卸载版本后不能含有多个版本,否则无法卸载。 > 卸载模块的时候必须调用`JModuleManager`的`unload`方法实现,虽然该接口提供了查找`JModule`的方法,并且`JModule`存在`destroy()`方法,但是我们依然不建议直接调用模块的的销毁方法,避免管理器模块状态不一致出现错误。 ## 2.7 spring `JModuleLink`除了默认的模块管理器实现,也针对`spring`提供了`spring`的模块管理器实现。使用`spring`的模块管理器,我们仅需要将`JModuleManagerImpl `换成`JSpringModuleManager `,一般情况下,我们会将该对象实例交给`spring`管理。 另外,与默认的通用实现不同的地方还有,在加载模块的时候,建议使用`JSpringModuleConfig `作为模块信息的配置类。`JSpringModuleConfig`在`JModuleConfig `的基础上增加了3个配置项。配置说明如下: | 名称 | 说明 | | :---------: | :---------------------------------------------------: | | properties | 模块里的bean需要的配置信息,集成了`spring properties` | | xmlPatterns | 需要加载的 XML配置 | | exclusions | 需要排除的配置文件 | > 需要注意的是,如果配置中我们添加了扫描的包,在初始化spring模块的上下文的时候将使用注解的形式扫描,否则使用XML方式。 ## 2.8 Web环境 考虑到有些时候可能会将`JModuleLink`用于Web环境。为了方便操作,提供了Web专用的模块管理器,子模块可以共享程序上下文属性(只读),使用Web环境,我们需要把管理器换成`JWebModuleManager`,在编写`Action`的时候,我们可以直接继承`JServletAction`或`JHttpServletAction`,这两个实现本质上都是`Servlet`,这样我们就可以像编写普通`Servlet`代码一样编写模块的`Action`。管理器会在模块初始化和销毁的时候回调其对应的方法(与传统`Servlet`保持一致)。 需要特殊注意的是使用`JWebModuleManager`加载的模块为`JWebModule`,在原有模块的基础上提供了`doServletAction`和`doDefaultAction`方法,可以直接传递请求和响应对象。 Web环境中对应的配置类为`JWebModuleConfig`,在默认的`JModuleConfig`基础上新增了如下参数: | 名称 | 说明 | | :-------------------: | :----------------------------------------------------------: | | initParameters | 上下文参数 | | servletInitParameters | `Servlet`参数,键值为`Action`的名字 | | webRootPath | Web根目录,会在该目录下按照标准war解压后的文件结构添加类路径 | ## 2.9 日志 因为`JModuleLink`的大部分常用功能不需要依赖任何三方包,所以提供了一种轻量的日志方式,默认会在控制台输出日志内容,我们可以按照实际集成的日志三方包,编写日志的实现类,然后调用`JLogFactory`的`setImplementation `方法设置日志实现类,该类需要实现`JLog `接口。 > 从1.0.2版本开始默认集成了`JLogging`,如果不引用该包,用法与原来相同。引用后日志将会使用`Jlogging`实现,原有代码无需修改。