DP项目创建之初作为中间层处理WMS系统的报表数据,正常WMS数据是SQL直接返回结果到WMS展示,如果需要一些特殊处理,比如多个数据库的数据整合,复杂数据计算等需求,则使用DP中转处理。
后续添加了BPM对接CBS资金系统的功能,同样是作为中转,比如BPM流程到某个节点,调用DP的接口,由DP向CBS发起付款申请,并记录日志和管理申请结果,根据申请结果和付款结果再调用BPM的接口审批流程。中转的原因是BPM项目限制过多,且架构太老、部署复杂,开发不方便,所以放到DP来完成主要的业务操作。
一、自定义注解实现数据源切换
自定义注解
@Target(ElementType.METHOD)// 用于方法上
@Retention(RetentionPolicy.RUNTIME)//运行时保留
@Documented
public @interface DataSource {
/**
* 目标 datasource
* @return datasource
*/
DataSourceEnum value();
}
@Getter
public enum DataSourceEnum {
WMS("wms",1),
MES("mes",2),
SAP("sap",7),
BPM("bpm",8),
UNKNOWN("unknown",-1),
;
private final String key;
private final Integer value;
DataSourceEnum(String key, Integer value) {
this.key = key;
this.value = value;
}
public static DataSourceEnum getByValue(Integer value){
for (DataSourceEnum mapping : DataSourceEnum.values()) {
if (Objects.equals(mapping.getValue(), value)){
return mapping;
}
}
return UNKNOWN;
}
}
切面类
ProceedingJoinPoint 类
ProceedingJoinPoint 是 AspectJ 框架提供的接口,它是环绕通知(@Around)中使用的特殊类型的连接点。其他通知使用JoinPoint即可。
作用:
- 代表被拦截的方法执行点
- 在环绕通知中,允许控制是否执行目标方法
- 提供对目标方法的完全控制,包括修改参数、处理返回值、捕获异常等
主要方法: - proceed() - 执行目标方法
- proceed(Object[] args) - 使用指定参数执行目标方法
- getSignature() - 获取方法签名
- getTarget() - 获取目标对象
- getArgs() - 获取方法参数
joinPoint.proceed() 方法调用
这行代码是环绕通知中最关键的部分。
作用:
实际执行被拦截的目标方法
如果不调用 proceed(),目标方法将不会被执行
返回目标方法的执行结果
@Slf4j
@Aspect
@Component
public class DataSourceAspect {
@Around("@annotation(com.lzcer.dataprocessor.frame.annotation.DataSource)")//环绕通知,拦截所有添加了@DataSource注解的方法
public Object changeDataSource(ProceedingJoinPoint joinPoint) throws Throwable {//ProceedingJoinPoint 用于获取对目标方法的完全控制
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//获得目标方法签名
Method method = signature.getMethod();//得到目标方法
DataSource annotation = method.getAnnotation(DataSource.class);//获取实例注解,用于解析用到的数据源
DataSourceEnum datasource = annotation.value();
log.info("========change datasource : {}========", datasource.getKey());
DynamicDataSourceContextHolder.setDataSource(datasource.getKey());
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object result = joinPoint.proceed();
stopWatch.stop();
log.info("method: {} time cost :{} ms",method.getName(),stopWatch.getTotalTimeMillis());
return result;
}
}
设置当前数据源,使用ThreadLocal存储当前线程使用的数据源
public class DynamicDataSourceContextHolder {
// 使用ThreadLocal存储每个线程的数据源标识
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
// 设置当前线程的数据源标识
public static void setDataSource(String dataSourceKey) {
CONTEXT_HOLDER.set(dataSourceKey);
}
// 获取当前线程的数据源标识
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
// 清除当前线程的数据源标识
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
切换数据源的核心实现
Spring 提供的 AbstractRoutingDataSource 是实现动态数据源切换的核心类
它的核心机制是通过 determineCurrentLookupKey() 方法获取当前应该使用的数据源标识,然后在已配置的数据源映射中查找对应的实际数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(Object defaultDataSource, Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSource();
}
}
数据源配置
@Data
@Component
@ConfigurationProperties("datasource")//通过@ConfigurationProperties注解将配置文件中的 datasource 部分映射为 Java 对象
public class DataSourceConfig {
private Map<String, DbConfig> dbs;
@Data
public static class DbConfig {
private String url;
private String username;
private String password;
private String driverClassName;
}
}
@AllArgsConstructor
@Configuration
public class DataSourceManager {
private static final Map<Object, Object> DATA_SOURCE_MAP = new HashMap<>();
private final DataSourceConfig config;
@Bean(name = "dynamicDataSource")
@Primary//优先选择
public DynamicDataSource createDynamicDataSource() {
// 遍历配置文件中的所有数据源配置
config.getDbs().forEach((k, v) -> {
if (DataSourceEnum.SAP.getKey().equals(k)) {
// SAP 数据源使用 HikariDataSource
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName(v.getDriverClassName());
dataSource.setJdbcUrl(v.getUrl());
dataSource.setUsername(v.getUsername());
dataSource.setPassword(v.getPassword());
DATA_SOURCE_MAP.put(k, dataSource);
} else {
// 其他数据源使用 DruidDataSource
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(v.getDriverClassName());
dataSource.setUrl(v.getUrl());
dataSource.setUsername(v.getUsername());
dataSource.setPassword(v.getPassword());
List<Filter> filters = new ArrayList<>();
filters.add(statFilter());
filters.add(wallFilter());
dataSource.setProxyFilters(filters);
try {
dataSource.setFilters("stat,wall,slf4j");
} catch (SQLException e) {
e.printStackTrace();
}
DATA_SOURCE_MAP.put(k, dataSource);
}
});
// 创建 DynamicDataSource,传入默认数据源和所有数据源映射
return new DynamicDataSource(DATA_SOURCE_MAP.get("wms"), DATA_SOURCE_MAP);
}
}
总结
实现数据源切换主要是用到了Spring框架提供的AbstractRoutingDataSource类,需要继承这个类,重写里面的determineCurrentLookupKey()方法。
- 需要在配置文件中配置号多个数据源的url、username、password、DriverClass等
- 新建DataSourceConfig类通过@ConfiguationProperties注解将配置文件中的配置变成JAVA中的对象
- 在 DataSourceManager 中创建实际的数据源对象
- DynamicDataSource初始化,DynamicDataSource 继承 AbstractRoutingDataSource,在初始化时设置默认数据源和目标数据源映射
- 运行时切换数据源,DataSourceAspect 切面通过 @Around(“@annotation(com.lzcer.dataprocessor.frame.annotation.DataSource)”) 拦截被 @DataSource 注解标记的方法
- DynamicDataSourceContextHolder.setDataSource() 将数据源键值存储到 ThreadLocal 中
- 执行目标方法触发数据源选择,当方法中有数据库操作时触发,调用我们实现的 determineCurrentLookupKey(),获取实际的数据库源,返回数据库源
- 执行数据库操作