一、背景
最近在使用 Springboot + Activiti6 重构和优化审批流程闭环管理的项目,前期由于该项目会不停的将软件应用到不同的用户与场景,同时可预知的未来可能还会存在各种用户的各种特定需求,所以为了以后的软件升级和代码维护,所以我准备使用插件的模式来做项目的设计。
不同用户的项目核心程序使用一套统一的核心逻辑代码,特定功能通过不同的用户插件实现特定功能,不影响其他用户版本的软件功能,核心代码通过接口的方式为插件提供功能接入,在应对不同用户的不同需求的时候只需要扩展接口开发插件,在插件中实现具体的核心功能逻辑即可。
因此在插件框架的选择上我选择了pf4j,因为使用起来比较简单。pf4j每个插件通过不同的ClassLoader将对应插件内的接口实现类加载到程序中。
二、项目设计
按照上面的思路,我将项目分为了三个模块
- activiti-core
负责整合项目的核心逻辑,包括前端接口,流程处理逻辑,与插件交互逻辑等应用的核心逻辑 - activiti-plugin-api
用来定义插件应该实现的接口,activiti-core 通过该模块与所有实现了这些插件api的插件进行交互。 - activiti-plugin-xxx
所有实现了 activiti-plugin-api 对应扩展接口的插件集合,用于处理不同用户的特定的业务逻辑。(包括不同要求的任务通知、不同要求的审批单生成逻辑等)
三、存在的问题
首先,根据上面的设计,所有的插件是不应该引用任何activiti-core的代码,在插件层面应该是对activiti-core模块没有任何感知的,插件只应该通过实现对应的 activiti-api 接口达到与activiti-core 交互的目的。
其次,具体的流程文件定义是在插件中的,流程中很大概率会存在类似于 serviceTask
这样的任务节点,比如:
<serviceTask id="servicetask1" name="文档上传" activiti:class="com.anke.activiti.plugin.prodef.jisan.UploadFileService">
<extensionElements>
<activiti:field name="uploadFile">
<activiti:expression><![CDATA[${warnNoticeDoc}]]></activiti:expression>
</activiti:field>
</extensionElements>
</serviceTask>
这里我定义了一个文件上传的serviceTask节点,指定了使用 com.anke.activiti.plugin.prodef.jisan.UploadFileService
这个类来进行处理。
ClassNotFoundException
但是,由于PF4J每一个插件使用不同的ClassLoader,而Activiti的核心处理逻辑在activiti-core这个模块里面,这就会导致流程处理在执行到该节点的时候报错,由于不在同一个ClassLoader下,所以无法找到该节点的处理类。
Caused by: org.activiti.engine.ActivitiClassLoadingException: Class not found: com.anke.activiti.plugin.prodef.jisan.UploadFileService
at org.activiti.engine.impl.util.ReflectUtil.loadClass(ReflectUtil.java:87)
at org.activiti.engine.impl.util.ReflectUtil.instantiate(ReflectUtil.java:134)
... 122 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.anke.activiti.plugin.prodef.jisan.UploadFileService
at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader.loadClass(TomcatEmbeddedWebappClassLoader.java:72)
at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1188)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at org.activiti.engine.impl.util.ReflectUtil.loadClass(ReflectUtil.java:288)
at org.activiti.engine.impl.util.ReflectUtil.loadClass(ReflectUtil.java:68)
... 123 common frames omitted
四、解决问题
既然问题是plugin的ClassLoader和核心模块的ClassLoader不是同一个导致的,那么想办法把他们结合起来就可以把问题解决了。
Activiti 的 EngineConfiguration 类中可以设置ClassLoader,这就有了解决的方法。
我们自定义一个ClassLoader,首先找到当前模块中加载的所有的插件,找到它们对应的ClassLoader,然后这个classLoader在查找类的时候优先从父ClassLoader中查找,如果查找不到接着去这些个ClassLoader中查找,这样就能解决插件中的类无法找到的问题了。
下面是解决代码 (activiti-core)
import com.anke.activiti.domain.activiti.custom.CommonCustomFormType;
import com.anke.activiti.domain.activiti.custom.FileCustomFormType;
import org.activiti.engine.form.AbstractFormType;
import org.activiti.spring.SpringAsyncExecutor;
import org.activiti.spring.SpringProcessEngineConfiguration;
import org.activiti.spring.boot.AbstractProcessEngineAutoConfiguration;
import org.pf4j.PluginManager;
import org.pf4j.PluginWrapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Mengfly
*/
@Configuration
public class ActivityConfiguration extends AbstractProcessEngineAutoConfiguration {
@Value("${spring.datasource.url}")
private String url;
@Value(("${spring.datasource.driver-class-name}"))
private String driverClass;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Bean
public SpringProcessEngineConfiguration springProcessEngineConfiguration(
PlatformTransactionManager transactionManager, SpringAsyncExecutor springAsyncExecutor,
PluginManager pluginManager) throws IOException {
// 设置Activiti的数据源
DataSource build = DataSourceBuilder.create()
.url(url).username(username).password(password).driverClassName(driverClass)
.build();
SpringProcessEngineConfiguration springProcessEngineConfiguration =
baseSpringProcessEngineConfiguration(build, transactionManager, springAsyncExecutor);
// 设置流程引擎的ClassLoader为我们自定义的ClassLoader
springProcessEngineConfiguration.setClassLoader(buildPluginClassLoader(pluginManager));
List<AbstractFormType> customFormTypes = new ArrayList<>();
customFormTypes.add(new FileCustomFormType());
customFormTypes.add(new CommonCustomFormType());
// 添加自定义的参数类型
springProcessEngineConfiguration.setCustomFormTypes(customFormTypes);
return springProcessEngineConfiguration;
}
/**
* 自定义的ClassLoader,查找类的流程如下:
* 1. 到父ClassLoader中查找
* 2. 如果查找失败继续到各个插件中去查找,如果各个插件中也查找不到抛出ClassNotFoundException
*/
private ClassLoader buildPluginClassLoader(PluginManager pluginManager) {
List<PluginWrapper> plugins = pluginManager.getPlugins();
List<ClassLoader> totallyClassLoaderList = plugins.stream().map(PluginWrapper::getPluginClassLoader).collect(Collectors.toList());
return new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 先从 父 classloader 中查找类
try {
return super.findClass(name);
} catch (Exception e) {
// 如果父类中查找不到,从这些plugin中去查找
for (ClassLoader classLoader : totallyClassLoaderList) {
try {
return classLoader.loadClass(name);
} catch (Exception ignored) {
// ignore
}
}
}
throw new ClassNotFoundException("Can't found class " + name);
}
};
}
}