欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Boot教程十四:基于自定义注解的AOP数据源自动切换

程序员文章站 2022-05-01 08:08:50
...

上一篇文章讲到了多数据源的配置和手动切换,手动切换费时费力,下面我们改进一下,改成基于注解的AOP数据源自动切换。

基础知识不在赘述,直接上代码:


public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    /*
      *管理所有的数据源id;
      *主要是为了判断数据源是否存在;
      */
    public static List<String> dataSourceIds = new ArrayList<String>();

    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    public static String getDataSourceType() {
        return contextHolder.get();
    }

    public static void clearDataSourceType() {
        contextHolder.remove();
    }

    public static boolean containsDataSourceType(String DataSourceId) {
        return dataSourceIds.contains(DataSourceId);
    }
}



/**
 * @author Shuyu.Wang
 * @package:com.ganinfo.datasource
 * @className:
 * @description:动态数据源(需要继承AbstractRoutingDataSource)
 * @date 2018-08-01 15:47
 **/

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        System.err.println("DataSourceContextHolder.getDataSourceType()       "+DataSourceContextHolder.getDataSourceType());
        return DataSourceContextHolder.getDataSourceType();
    }
}




@Aspect
@Order(-10)//保证该AOP在@Transactional之前执行
@Component
public class DynamicDataSourceAspect {

    @Pointcut("@annotation(com.ganinfo.datasource.TargetDataSource)")
    public void annotationPointcut(){
        System.out.println("======我是一个切入点");
    };
    /*
        * @Before("@annotation(ds)")
        *的意思是:
        * @Before:在方法执行之前进行执行:
        * @annotation(targetDataSource):
        *会拦截注解targetDataSource的方法,否则不拦截;
        */
    @Before("annotationPointcut()")
    public void changeDataSource(JoinPoint joinPoint) throws Throwable {
        MethodSignature signature=(MethodSignature) joinPoint.getSignature();
        Method method=signature.getMethod();
        TargetDataSource targetDataSource=method.getAnnotation(TargetDataSource.class);
        System.out.println("注解的拦截方法名注解内容前:"+targetDataSource.value());
        //获取当前的指定的数据源;
        String dsId = targetDataSource.value();
        //如果不在我们注入的所有的数据源范围之内,那么输出警告信息,系统自动使用默认的数据源。
        if (!DataSourceContextHolder.containsDataSourceType(dsId)) {
            System.err.println("数据源[{}]不存在,使用默认数据源 > {}" + targetDataSource.value() + joinPoint.getSignature());
        } else {
            System.out.println("UseDataSource : {} > {}" + targetDataSource.value() +"====="+ joinPoint.getSignature());
            //找到的话,那么设置到动态数据源上下文中。
            DataSourceContextHolder.setDataSourceType(targetDataSource.value());
            System.out.println("数据源切换为:"+ DataSourceContextHolder.getDataSourceType());
        }
    }

    @After("annotationPointcut()")
    public void restoreDataSource(JoinPoint joinPoint) {
        MethodSignature signature=(MethodSignature) joinPoint.getSignature();
        Method method=signature.getMethod();
        TargetDataSource targetDataSource=method.getAnnotation(TargetDataSource.class);
        System.out.println("注解的拦截方法名注解内容前:"+targetDataSource.value());
        //方法执行完毕之后,销毁当前数据源信息,进行垃圾回收。
        DataSourceContextHolder.clearDataSourceType();

        System.out.println("lear后:"+ DataSourceContextHolder.getDataSourceType());

    }


}



@Configuration // 该注解类似于spring配置文件
public class MyBatisConfig {
    @Autowired
    private Environment env;

    private String MYBATIS_CONFIG = "config/mybatis.xml";

    /**
     * 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称)
     */
    @Bean("masterDbDataSource")
    public DataSource masterDbDataSource() throws Exception {
        Properties props = new Properties();
        props.put("driverClassName", env.getProperty("spring.datasource.driverClassName"));
        props.put("url", env.getProperty("spring.datasource.url"));
        props.put("username", env.getProperty("spring.datasource.username"));
        props.put("password", env.getProperty("spring.datasource.password"));
        return DruidDataSourceFactory.createDataSource(props);
    }

    @Bean("slaveDb2DataSource")
    public DataSource slaveDb2DataSource() throws Exception {
        Properties props = new Properties();
        props.put("driverClassName", env.getProperty("custom.datasource.ds1.driverClassName"));
        props.put("url", env.getProperty("custom.datasource.ds1.url"));
        props.put("username", env.getProperty("custom.datasource.ds1.username"));
        props.put("password", env.getProperty("custom.datasource.ds1.password"));
        return DruidDataSourceFactory.createDataSource(props);
    }

    /**
     * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
     * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
     */
    @Bean
    @Primary
    public DynamicDataSource dataSource(@Qualifier("masterDbDataSource") DataSource masterDbDataSource, @Qualifier("slaveDb2DataSource") DataSource slaveDb2DataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("masterDbDataSource", masterDbDataSource);
        targetDataSources.put("slaveDb2DataSource", slaveDb2DataSource);
        DataSourceContextHolder.dataSourceIds.add("masterDbDataSource");
        DataSourceContextHolder.dataSourceIds.add("slaveDb2DataSource");
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法
        dataSource.setDefaultTargetDataSource(slaveDb2DataSource);// 默认的datasource设置为masterDbDataSource
        return dataSource;
    }

    /**
     * 根据数据源创建SqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDbDataSource") DataSource masterDbDataSource, @Qualifier("slaveDb2DataSource") DataSource slaveDb2DataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
     /*   sqlSessionFactoryBean.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));//*/
        /** 设置mybatis configuration 扫描路径 */
        System.out.println(env.getProperty("mybatis.configLocation"));
        sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(MYBATIS_CONFIG));
        // 指定数据源(这个必须有,否则报错)
        sqlSessionFactoryBean.setDataSource(this.dataSource(masterDbDataSource, slaveDb2DataSource));
        /** 添加mapper 扫描路径 */
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 配置事务管理器
     */
    @Bean
    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
        return new DataSourceTransactionManager(dataSource);
    }
}


/**
 * @author Shuyu.Wang
 * @package:com.ganinfo.datasource
 * @className:
 * @description:在方法上使用,用于指定使用哪个数据源
 * @date 2018-08-01 20:56
 **/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String value();
}




package com.ganinfo.datasource.mapper;

import com.ganinfo.datasource.TargetDataSource;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

/**
 * @author Shuyu.Wang
 * @package:com.ganinfo.datasource.mapper
 * @className:
 * @description:
 * @date 2018-08-01 16:03
 **/
@Mapper
public interface Test {

    @TargetDataSource(value ="masterDbDataSource")
    @Select("SELECT name FROM t_user limit 1")
    List<Map<String,Object>> getList();


    @TargetDataSource(value ="slaveDb2DataSource")
    @Select("SELECT * FROM t_base_place limit 1")
    List<Map<String,Object>> getList2();
}




@Service
public class TestService {
    @Autowired
    private Test test;

    @TargetDataSource(value = "masterDbDataSource")
   public  List<Map<String, Object>> getList() {
        return test.getList();
    }


//    @TargetDataSource(value = "ds1")
    @TargetDataSource(value ="slaveDb2DataSource")
    public  List<Map<String, Object>> getList2() {
        return test.getList2();
    }


}







#主数据源,默认的
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://172.16.1.28:3306/closedauth?zeroDateTimeBehavior=convertToNull&amp;useUnicode=true&amp;characterEncoding=utf-8
spring.datasource.username = cweserver
spring.datasource.password = cweserveryzhh

#更多数据源
custom.datasource.names=ds1
custom.datasource.ds1.driverClassName = com.mysql.jdbc.Driver
custom.datasource.ds1.url = jdbc:mysql://172.16.1.28:3306/closeddata?zeroDateTimeBehavior=convertToNull&amp;useUnicode=true&amp;characterEncoding=utf-8
custom.datasource.ds1.username = cweserver
custom.datasource.ds1.password = cweserveryzhh

这里切记注解要在service方法中加,不能再mapper中添加,不然无法被aop拦截;

测试方法如下:


@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class TestTest {
    @Autowired
    private com.ganinfo.datasource.mapper.Test test;
    @Autowired
    private TestService testService;

    @Test
    public void getList2() throws Exception {
        List<Map<String, Object>> list = testService.getList2();
        System.out.println(GsonUtil.GsonString(list));
//        log.info("");
    }
    @org.junit.Test
    public void getList() throws Exception {
        List<Map<String, Object>> list = testService.getList();
        System.out.println(GsonUtil.GsonString(list));
    }



}