需求
- 系统架构是spring framework +各种组件
- 项目重构 打算使用mybatis 作为orm框架
- 项目依赖多个数据源、多个数据源需要读写分离
- 抛弃xml配置,完全注解化
问题
-
注解扫mapper过程中,一个mapper只能被一个sqlsessiontemplate装载 ,若想实现读写分离需要对每个库表定义不同的读写mapper,然后通过@MapperScan(basePackages = "mybatisSpring.mapper" sqlSessionTemplateRef = "") 分别指定数据源 。类似图中样式,basePackages分别指定namespace 与testmapper 然后再指定对应的sqlSessionTemplate。
上述方式实现和逻辑比较简单,但是太粗糙,写的代码会很多。
理想使用方式 mapper层按库表逻辑划分,与业务逻辑完全剥离。
实现方案
- 使用AbstractRoutingDataSource进行多数据源管理
- 使用切面完成数据源的动态切换
- 直接引用sqlSessionTemplate完成读写操作
show me the code
-
代码结构
- annotation 自定义注解
- conf java配置文件
- dao 封装的dao层服务
- mapper mybatis的接口mapper类
- model DO层
- 具体代码
- AbstractRoutingDataSource 的实现类,管理项目的所有数据源。
public class MomentRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<DataSourceType> dataSourceHolder = new ThreadLocal<>();
public static void setDataSource(DataSourceType dataSourceType) {
dataSourceHolder.set(dataSourceType);
}
public static DataSourceType getDataSource() {
return dataSourceHolder.get();
}
public static void clear() {
dataSourceHolder.remove();
}
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
}
- spring 配置文件
@Configuration
@MapperScan(basePackages = "mybatisSpring.mapper" )
@ComponentScan(basePackages = "mybatisSpring")
// 开启切面
@EnableAspectJAutoProxy
public class SqlSessionTemplateConf {
/**
* 初始化dataSource
*
* @return
* @throws Exception
*/
private HikariDataSource getDataSource(String dbname) throws Exception {
HikariDataSource dataSource = new HikariDataSource();
String jdbcUrl = String.format("jdbc:mysql://%s:%s/%s?zeroDateTimeBehavior=CONVERT_TO_NULL&autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"
, "localhost", 3306, "mpaper_cms2");
dataSource.setJdbcUrl(jdbcUrl);
dataSource.setUsername("*******");
dataSource.setPassword("******");
dataSource.setMinimumIdle(10);
dataSource.setMaximumPoolSize(20);
//等待获取连接时间-默认30s,超过配置阈值抛异常
dataSource.setConnectionTimeout(500);
dataSource.setPoolName(dbname);
return dataSource;
}
@Bean
public DataSource momentRoutingDataSource() throws Exception {
MomentRoutingDataSource momentRoutingDataSource = new MomentRoutingDataSource();
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DataSourceType.CMS_RO, getDataSource("cms_ro"));
targetDataSource.put(DataSourceType.CMS_RW, getDataSource("cms_rw"));
momentRoutingDataSource.setTargetDataSources(targetDataSource);
return momentRoutingDataSource;
}
@Bean("momentSqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(DataSource momentRoutingDataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(momentRoutingDataSource);
return new SqlSessionTemplate(factoryBean.getObject());
}
}
- 切面实现& 定义注解
@Aspect
@Component
@Order(0)
public class CustomDataSourceAspect {
// @annotation(mybatisSpring.annotation.DataSource) &&
@Pointcut(value = "execution( * mybatisSpring.dao..*.*(..))")
public void pointCut() {
}
@Before(value = "pointCut()")
public void changeDataSource(JoinPoint joinPoint) throws NoSuchMethodException {
Class<?> aClass = joinPoint.getTarget().getClass();
//拦截mapper方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (method != null && method.isAnnotationPresent(DataSource.class)) {
DataSource dataSource = method.getAnnotation(DataSource.class);
MomentRoutingDataSource.setDataSource(dataSource.type());
System.out.println("Service Class 数据源切换至--->" + dataSource.type().getValue());
return;
}
//拦截mapper类
if (aClass.isAnnotationPresent(DataSource.class)) {
DataSource dataSource = aClass.getAnnotation(DataSource.class);
MomentRoutingDataSource.setDataSource(dataSource.type());
System.out.println("Service Class 数据源切换至--->" + dataSource.type().getValue());
return;
}
}
/**
* 方法结束后
*/
@After(value = "pointCut()")
public void afterReturning() throws Throwable {
try {
MomentRoutingDataSource.clear();
System.out.println("数据源已移除!");
} catch ( Exception e ) {
e.printStackTrace();
System.out.println("数据源移除报错!");
}
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
DataSourceType type() default DataSourceType.CMS_RO;
}
-dao 层代码
@Service
@DataSource(type = DataSourceType.CMS_RO)
public class NewsTvInfoDao {
@Autowired
private SqlSessionTemplate momentSqlSessionTemplate;
public NewsTvInfo getNewsTvInfo(int id) {
NewsTvInfoMapper mapper = momentSqlSessionTemplate.getMapper(NewsTvInfoMapper.class);
return mapper.findByVid(id);
}
@DataSource(type = DataSourceType.CMS_RW)
public NewsTvInfo getNewsTvInfoRw(int id) {
ActivityMapper mapper = momentSqlSessionTemplate.getMapper(ActivityMapper.class);
return mapper.findByVid(id);
}
}
- main 入口
public class MybatieSpringMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SqlSessionTemplateConf.class);
// 方法前未添加注解,取类的注解注入的数据源
NewsTvInfoDao dao = context.getBean(NewsTvInfoDao.class);
NewsTvInfo newsTvInfo = dao.getNewsTvInfo(9102910);
System.out.println(newsTvInfo.getBigPic());
// 方法添加注解,取方法的注解注入的数据源
NewsTvInfo newsTvInfoRw = dao.getNewsTvInfoRw(9102910);
System.out.println(newsTvInfoRw.getBigPic());
}
}
说明
- 代码未列完整,依赖mybatis ,mybatis-spring 依赖包
- 切面逻辑中,会先扫描方法后扫描类。因此方法添加的注解的优先级大于类的优先级,因此可以在dao层类的上方添加默认数据源。
- @EnableAspectJAutoProxy需要 添加此注解