业务与读写分离
背景:项目是比较大的业务与数据操作都冗杂在一起的系统,现在需要进行数据读写的业务抽离,原来没有集成mybatis等框架,全部使用DatabaseUtil类来获取jdbc连接,当然这个连接是有数据库连接池在启动的时候 就好了的,每次拿connection,但相较于mybatis等优秀框架还是差别很大,故集成。
使用方式:现有系统集成mybatisplus+hikari+使用注解实现多数据源切换方式。
主要更改内容:
DataBaseEnum:现有三个数据源枚举名称。
DataBaseSource:创建@DataBaseSource注解,标注在方法或者类上,动态切换数据源。
DataBaseSourceAspect:切换数据源过程,找到切点,判断使用什么数据库,切换数据源。
DataSourceContextHolder:用ThreadLocal(线程内部的存储类,提供线程内存储变量的功能)保存数据源的信息到线程中,方便使用获取数据源信息。
DynamicDataSource:继承AbstractRoutingDataSource(根据用户规则选择当前数据源),在每次数据库查询操作前执行determineCurrentLookupKey()决定使用哪个数据源。
HikariPoolConfig:初始化数据库配置,创建数据源,加入配置,加入到动态数据源内。
MybatisConfig:mybatisplus的配置类,创建session工厂。主要是为了解决,mybatisplus空字段不返回问题,setCallSettersOnNulls设为true。
SpringUtils:非spring管理的类获取bean对象的公共方法。
DatabaseInfoHandleUtils:原数据库操作查询后的数据经过了一定的代码处理,这个类主要为了处理mybatis查询到的数据,代码处理后与原先数据保持不变。(按照自己原有逻辑写就可以)
com.xxxx.xxx.dao主要写从业务里抽离出来的sql查询方法。
其他修改点就是 将原来所有sql抽取出来,放入dao里面。
话不多说,贴代码:
DataBaseEnum
package com.xxx.xxx.framework.datasource;
/**
*
* 项目里面用了三个数据库
* @author liutt
* @date 2020/09/15
*/
public enum DataBaseEnum {
/**
* 对应xxx.mysqlUrls
*/
STATISTICS,
/**
* 对应xxx.statistics.mysqlUrls
*/
ETERNAL,
/**
* xxxx.mysqlUrls
*/
TESTCASE;
}
DataBaseSource 注解类
package com.xxx.xxx.framework.datasource;
import java.lang.annotation.*;
/**
*
* DataBaseSource annotation
*
* @author liutt
* @date 2020/09/10
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface DataBaseSource {
DataBaseEnum value() default DataBaseEnum.STATISTICS;
}
DataSourceContextHolder
package com.xxx.xxx.framework.datasource;
/**
* 设置路由的目的是为了方便查找对应的数据源
* 可以用ThreadLocal保存数据源的信息到每个线程中
* 方便使用获取
*
* Gets and sets the context environment.
* It is mainly responsible for changing the name of the context data source
*
* @author liutt
* @date 2020/09/10
*/
class DataSourceContextHolder {
private DataSourceContextHolder() {
}
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
static void setDataSource(String dbType) {
CONTEXT_HOLDER.set(dbType);
}
static String getDataSource() {
return CONTEXT_HOLDER.get();
}
static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
DataBaseSourceAspect 切面类
package com.xxx.xxx.framework.datasource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* AOP switch datasource procedure
*
* @author liutt
* @date 2020/09/10
*/
@Aspect
@Component
public class DataBaseSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DataBaseSourceAspect.class);
@Pointcut("@annotation(com.xxx.xxx.framework.datasource.DataBaseSource)")
public void dbPointCut() {
}
@Before("dbPointCut()")
public void beforeSwitchDataSource(JoinPoint joinPoint) {
//获取当前访问的class
Class<?> className = joinPoint.getTarget().getClass();
String dataSource;
DataBaseSource annotation = null;
try {
if (className.isAnnotationPresent(DataBaseSource.class)) {
annotation = className.getAnnotation(DataBaseSource.class);
} else {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
if (method.isAnnotationPresent(DataBaseSource.class)) {
annotation = method.getAnnotation(DataBaseSource.class);
}
}
dataSource = annotation == null ? DataBaseEnum.STATISTICS.name() : annotation.value().name();
DataSourceContextHolder.setDataSource(dataSource);
} catch (Exception e) {
logger.info(e.getMessage(), e);
}
}
@After("dbPointCut()")
public void afterSwitchDataSource(JoinPoint joinPoint) {
DataSourceContextHolder.clearDataSource();
}
}
HikariPoolConfig
备注:ConfigHelper是为了获取配置文件中的参数,只需要得到你的参数值就可以。
package com.xxx.xxx.framework.datasource;
import com.xxx.xxx.framework.config.ConfigHelper;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 初始化数据库配置 创建数据源 加入配置
* 加入到动态数据源内
*
* @author liutt
* @date 2020/09/10
*/
@Configuration
public class HikariPoolConfig {
private static final Logger logger = LoggerFactory.getLogger(HikariPoolConfig.class);
static final String STATISTICS_DB = "test01";
static final String ETERNAL_DB = "test02";
static final String TEST_CASE_DB = "test03";
@Bean(name = STATISTICS_DB)
public static DataSource defaultDataSource() {
String url = ConfigHelper.getStatisticsSqlUrl();
String user = ConfigHelper.getMysqlUser();
String passWord = ConfigHelper.getMysqlPasswd();
return new HikariDataSource(setHikariConfigInfo(url, user, passWord));
}
@Bean(name = ETERNAL_DB)
public static DataSource eternalDataSource() {
String url = ConfigHelper.getEternalStatisticSqlUrl();
String user = ConfigHelper.getMysqlUser();
String passWord = ConfigHelper.getMysqlPasswd();
return new HikariDataSource(setHikariConfigInfo(url, user, passWord));
}
@Bean(name = TEST_CASE_DB)
public static DataSource testCaseDataSource() {
String url = ConfigHelper.getStatisticsTestCaseSqlUrl();
String user = ConfigHelper.getMysqlUser();
String passWord = ConfigHelper.getMysqlPasswd();
return new HikariDataSource(setHikariConfigInfo(url, user, passWord));
}
@Primary
@Bean(name = "dynamicDataSource")
public static DynamicDataSource dataSource(@Qualifier(STATISTICS_DB) DataSource statisticsDataSource,
@Qualifier(ETERNAL_DB) DataSource eternalDataSource,
@Qualifier(TEST_CASE_DB) DataSource testCaseDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataBaseEnum.STATISTICS.name(), statisticsDataSource);
targetDataSources.put(DataBaseEnum.ETERNAL.name(), eternalDataSource);
targetDataSources.put(DataBaseEnum.TESTCASE.name(), testCaseDataSource);
return new DynamicDataSource(statisticsDataSource, targetDataSources);
}
public static HikariConfig setHikariConfigInfo(String url, String user, String passWord) {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(50);
config.setMinimumPoolSize(4);
config.setDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlDataSource");
config.addDataSourceProperty("url", url);
config.addDataSourceProperty("encoding", ConfigHelper.getMysqlEncoding());
config.addDataSourceProperty("noAccessToProcedureBodies", "true");
config.addDataSourceProperty("user", user);
config.addDataSourceProperty("password", passWord);
return config;
}
}
DynamicDataSource
package com.xxx.xxx.framework.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
/**
* AbstractRoutingDataSource
* [Select the current datasource according to user rules]
*
* @author liutt
* @date 2020/09/10
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
}
MybatisConfig
package com.xxx.xxx.framework.datasource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
/**
* @author liutt
* @date 2020/09/22
*/
@Configuration
public class MybatisConfig {
@Bean("sqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactoryBean(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setCallSettersOnNulls(true);
bean.setConfiguration(configuration);
bean.setDataSource(dynamicDataSource);
return bean.getObject();
}
@Bean
@Primary
public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource")DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
SpringUtils
package com.xxx.xxx.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* Non spring managed classes call bean objects
*
* @author liutt
* @date 2020/09/11
*/
@Component
public class SpringUtils implements ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(SpringUtils.class);
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
}