Data Processer
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);}@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getDataSource();}
}
数据源配置
@Data
@Component
@ConfigurationProperties("datasource")//通过@ConfigurationProperties注解将配置文件中的 datasource 部分映射为 Java 对象
public class DataSourceConfig {private Map<String, DbConfig> dbs;@Datapublic 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 数据源使用 HikariDataSourceHikariDataSource 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 {// 其他数据源使用 DruidDataSourceDruidDataSource 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(),获取实际的数据库源,返回数据库源
- 执行数据库操作