为了支持业务代码尽量的解耦,把部分业务功能以插件的方式加载到主程序中,以满足组合式的部署。
我们的应用场景是这样的:公司集成了xxl-job调度框架,而调度框架分为,调度中心和执行器两部分。所有的任务业务代码都写在一个执行器里,则会造成代码重并且不利于各服务器部署组织。比如我有30个自动任务需要处理,一共有3台服务器(执行器),写在一起的话,我所有的执行器都需要加载30个任务,而改造分开后,则根据情况可以把1~10分配到第一台执行器中执行。11~20分配到第二台执行器中执行。其它分配到第三台执行器执行。这样业务就很清晰了。而且30个任务分别30个工程或模块管理,耦合度低,有利于代码管理 。
废话说的有点多,那就个代码吧。
首先,看一下目前代码结构,如下图:
1、ts-server-executor:为xxl-job的执行器是最终的运行web服务,其它任务会以jar包插件的形式加载到服务中。
2、ts-jobs:表示自动任务业务包
3、ts-jobs-common:表示公司的依赖包、类等。
4、ts-jobs-mapper:表示统一的数据库访问层代码。
5、ts-jobs-modules:表示自动任务业务模块包。
6、ts-jobs-demo和ts-jobs-logs为具体业务任务包。
package com.tsingsoft.executor.config;
import com.tsingsoft.executor.utils.ClassLoaderUtil;
import com.tsingsoft.executor.utils.PackageScanner;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.List;
/**
* 启动时注册bean核心类
* 此类用于动态加载jar中的类进行自动注册进系统。
* @author bask
* @version 1.0
* @date 2022/7/5
*
*/
@Slf4j
public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
/**
* 存储Jar文件基础路径
*/
private String basePath;
/**
* 包名称集,多个名称则通过","逗号进行区分。
*/
private String jarNames;
/**
* 包前缀,如:com.tsingsoft
*/
private String packagePrefix;
@SneakyThrows
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
try {
if(jarNames==null){
log.warn("加载包名称为空,如果需要加载则需要配置,请知晓!");
return;
}
String[] jarNameses = jarNames.split(",");
String[] packagePrefixes = packagePrefix.split(",");
for (int i = 0; i pluginClasses = scanner.getClassesNamesByJar(packagePrefix,url);
pluginClasses.stream().filter(x->x.startsWith(packagePrefix)).forEach(cls->{
log.info("pluginClass:{}",cls);
if(cls.startsWith(packagePrefix) && cls!=null){
Class clazz = null;
try {
clazz = classLoader.loadClass(cls);
//注册
registerBean(clazz, registry);
log.info("register bean [{}],Class [{}] success.", clazz.getName(), clazz);
} catch (Exception e) {
e.printStackTrace();
}
}else {
log.warn("存在空值》》》》》》》》》");
}
});
}
}
log.info("加载对应jar包成功!");
}catch (Exception e){
e.printStackTrace();
log.warn("指定插件目录没有加载对应合法jar包");
}
}
/**
* 註冊BEAN
* @param c
* @param registry
*/
private void registerBean(Class c, BeanDefinitionRegistry registry) {
String className = c.getName();
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(c);
BeanDefinition beanDefinition = builder.getBeanDefinition();
if (isSpringBeanClass(c)) {
registry.registerBeanDefinition(className, beanDefinition);
}
}
/**
* 方法描述 判断class对象是否带有spring的注解
* 存放實現
*
* @param cla jar中的每一个class
* @return true 是spring bean false 不是spring bean
* @method isSpringBeanClass
*/
public boolean isSpringBeanClass(Class cla) {
if (cla == null) {
return false;
}
//是否是接口
if (cla.isInterface()) {
return false;
}
//是否是抽象类
if (Modifier.isAbstract(cla.getModifiers())) {
return false;
}
try {
if (cla.getAnnotation(Component.class) != null) {
return true;
}
}catch (Exception e){
log.error("出现异常:{}",e.getMessage());
}
try {
if (cla.getAnnotation(Repository.class) != null) {
return true;
}
}catch (Exception e){
log.error("出现异常:{}",e.getMessage());
}
try {
if (cla.getAnnotation(Service.class) != null) {
return true;
}
}catch (Exception e){
log.error("出现异常:{}",e.getMessage());
}
return false;
}
/**
* 因加载顺序原因,则获取配置不用通过@Value来获取。
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
this.basePath = environment.getProperty("basePath");
this.packagePrefix = environment.getProperty("packagePrefix");
this.jarNames = environment.getProperty("jarNames");
}
}
通过在启动程序加载核心类,则可以把外部第三方jar包加载以loadclass中。
其中涉及一下配置:
server.port=18012
spring.main.allow-bean-definition-overriding=true
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/******?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&autoReconnect=true&useSSL=false
spring.datasource.username=*****
spring.datasource.password=******
# -----------动态加载jar配置------------------
# 加载存放jar的基础包名
basePath=E:/workspace/2022/ts-dynamic-project/plugins
# jar名称配置:多个jar用","区分
jarNames=ts-jobs-demo-0.0.1-SNAPSHOT.jar,ts-jobs-logs-0.0.1-SNAPSHOT.jar
# jar包代码基础路径
packagePrefix=com.tsingsoft
mybatis-plus.mapper-locations=classpath:com/tsingsoft/**/mapper/*.xml
# ---------------------------- xxl-job -------------------------------------
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://192.168.10.81:18011
### xxl-job, access token
xxl.job.accessToken=123456
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册,此处的AppName和界面设置执行器管理中AppName名,保持一致,这样才能完成自动注册。
xxl.job.executor.appname=ts-server-executor
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=
### 设置为0表示执行器端口随机分配,如果指定端口,则直接填写端口,如:9999
xxl.job.executor.port=${server.port}
#xxl.job.executor.port=9998
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30
其中工程中的plugins为打包后的jar包,如图:
打包的maven插件可参考下图: