SpringBoot 2.2.5 使用AOP方式配置多数据源动态切换,并支持事务,并解决内部方法调用时AOP切面失效的问题

说明

  1. 随着应用用户数量的增加,相应的并发请求的数量也会跟着不断增加,慢慢地,单个数据库已经没有办法满足我们频繁的数据库操作请求了。
  2. 在某些场景下,我们可能会需要配置多个数据源,使用多个数据源(例如实现数据库的读写分离)来缓解系统的压力等,同样的,SpringBoot官方提供了相应的实现来帮助开发者们配置多数据源,据我目前所了解到的,一般分为两种方式静态与动态(分包和AOP)。本文使用的是动态的方式。
  3. 因为我们控制是多数据源DataSource,而并不用关心到底是用哪种方式去使用,比如源生JDBC、MyBatis、Hibernate等上层的使用方法都是一样的。所以本文以本人常用的MyBatis-Plus操作数据源为例。
  4. 本文中数据库用的是mysql5.7。完整代码地址在结尾!!
  5. 动态方式,就是使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源,这样的入侵性较低,非常好的满足使用的需求。具体选择哪个数据源是由determineCurrentLookupKey()方法的返回值决定的,该方法需要我们继承AbstractRoutingDataSource来重写。

第一步,分别创建两个数据库db1,db2,sql如下

db1

CREATE DATABASE db1;

use db1;

CREATE TABLE user
(
    id INT NOT NULL COMMENT '主键ID',
    name VARCHAR(50) NULL DEFAULT NULL COMMENT '姓名',
    age INT NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
)CHARSET=utf8 ENGINE=InnoDB COMMENT '用户表';

INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

select * from user;

db2

CREATE DATABASE db2;

use db2;

CREATE TABLE task
(
    id INT NOT NULL COMMENT '主键ID',
    name VARCHAR(50) NULL DEFAULT NULL COMMENT '姓名',
    age INT NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
)CHARSET=utf8 ENGINE=InnoDB COMMENT '任务表';

INSERT INTO task (id, name, age, email) VALUES
(1, '李白', 18, 'test1@baomidou.com'),
(2, '露娜', 20, 'test2@baomidou.com'),
(3, '韩信', 28, 'test3@baomidou.com'),
(4, '橘右京', 21, 'test4@baomidou.com'),
(5, '百里玄刺', 24, 'test5@baomidou.com');

select * from task;

第二步,在pom.xml加入依赖,如下

<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

第三步,在application.yml配置多数据源,mybatis-plus相关配置

spring:
  # 配置数据源
  datasource:
    db1:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://xxx:3306/db1?useUnicode=true&characterEncoding=utf-8
      username: xxx
      password: xxx
    db2:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://xxx:3306/db2?useUnicode=true&characterEncoding=utf-8
      username: xxx
      password: xxx
  application:
    name: dynamicmultipledatasources-demo-server

# mybatis-plus相关配置
mybatis-plus:
  configuration:
    #不开启二级缓存
    cache-enabled: false
    # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
    map-underscore-to-camel-case: true
    # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
    call-setters-on-nulls: true
    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

server:
  port: 8189

第四步,将spring boot自带的DataSourceAutoConfiguration禁掉,因为它会读取application.yml文件的spring.datasource.*属性并自动配置单数据源。在@SpringBootApplication注解中添加exclude属性即可,如下

DynamicMultipleDataSourcesApplication

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class DynamicMultipleDataSourcesApplication {

    public static void main(String[] args) {
        SpringApplication.run(DynamicMultipleDataSourcesApplication.class, args);
    }

}

第五步,创建文件DynamicDataSource,DynamicDataSourceConfig,DataSourceContextHolder,MybatisPlusConfig,DataSourceType,如下

DynamicDataSource,用于继承AbstractRoutingDataSource来重写determineCurrentLookupKey()方法

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }

}

DynamicDataSourceConfig,用于配置数据源

import com.luoyu.dynamicmultipledatasources.enums.DataSourceType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
//basePackages 我们接口文件的地址
@MapperScan(basePackages = "com.luoyu.dynamicmultipledatasources.mapper", sqlSessionFactoryRef = "SqlSessionFactory")
public class DynamicDataSourceConfig {

    // 将这个对象放入Spring容器中
    @Bean("db1DataSource")
    // 表示这个数据源是默认数据源
    @Primary
    // 读取配置参数映射成为一个对象
    @ConfigurationProperties(prefix = "spring.datasource.db1", ignoreUnknownFields = false)
    public DataSource getDB1DateSource() {
        return DataSourceBuilder.create().build();
    }


    @Bean("db2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db2", ignoreUnknownFields = false)
    public DataSource getDB2DateSource() {
        return DataSourceBuilder.create().build();
    }


    @Bean("dynamicDataSource")
    public DynamicDataSource dynamicDataSource(@Qualifier("db1DataSource") DataSource db1DataSource,
                                        @Qualifier("db2DataSource") DataSource db2DataSource) {
        // 这个地方是比较核心的targetDataSource集合是我们数据库和名字之间的映射
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceType.DB1, db1DataSource);
        targetDataSource.put(DataSourceType.DB2, db2DataSource);
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSource);
        // 设置默认对象
        dataSource.setDefaultTargetDataSource(db1DataSource);
        return dataSource;
    }

    @Bean("transactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }

    @Bean("SqlSessionFactory")
    public SqlSessionFactory SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource, @Qualifier("mybatisplusConfiguration") org.apache.ibatis.session.Configuration configuration)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        // 使mybatis配置生效,加载顺序问题
        bean.setConfiguration(configuration);
        bean.setMapperLocations(
                // 设置我们的xml文件路径
                new PathMatchingResourcePatternResolver().getResources("classpath*:com/luoyu/dynamicmultipledatasources/mapper/xml/*.xml"));
        bean.setTypeAliasesPackage("com.luoyu.dynamicmultipledatasources.entity");
        return bean.getObject();
    }

}

DataSourceContextHolder,用于绑定当前线程的数据源

import com.luoyu.dynamicmultipledatasources.enums.DataSourceType;

public class DataSourceContextHolder {

    // 使用ThreadLocal保证线程安全
    private static final ThreadLocal<DataSourceType> TYPE = new ThreadLocal<>();

    /**
     * 往当前线程里设置数据源
     */
    public static void setDataSourceType(DataSourceType dataSourceType) {
        if (dataSourceType == null) {
            throw new NullPointerException();
        }
        TYPE.set(dataSourceType);
    }

    /**
     * 获取数据源
     */
    public static DataSourceType getDataSourceType() {
        DataSourceType dataSourceType = TYPE.get() == null ? DataSourceType.DB1 : TYPE.get();
        return dataSourceType;
    }

    /**
     * 清空数据源
     */
    public static void clearDataSourceType() {
        TYPE.remove();
    }

}

MybatisPlusConfig

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.annotation.Order;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @Description: MybatisPlus配置类
 * @Author: jinhaoxun
 * @Date: 2020/2/13 上午11:34
 * @Version: 1.0.0
 */
@EnableTransactionManagement
@Configuration
@Order(-1)
public class MybatisPlusConfig {

    /**
     * 使application.properties配置生效,如果不主动配置,由于@Order配置顺序不同,将导致配置不能及时生效,多数据源配置驼峰法生效
     * @return 数据源
     */
    @Bean("mybatisplusConfiguration")
    @ConfigurationProperties(prefix = "mybatis-plus.configuration")
    @Scope("prototype")
    public org.apache.ibatis.session.Configuration globalConfiguration() {
        return new org.apache.ibatis.session.Configuration();
    }

    /**
     * mybatis-plus SQL执行效率插件【生产环境可以关闭】,设置 dev test 环境开启
     */
//    @Bean
//    @Profile({"dev", "qa"})
//    public PerformanceInterceptor performanceInterceptor() {
//        PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
//        performanceInterceptor.setMaxTime(1000);
//        performanceInterceptor.setFormat(true);
//        return performanceInterceptor;
//    }

    /**
     * 分页插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        paginationInterceptor.setOverflow(false);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInterceptor.setLimit(500);
        // 开启 count 的 join 优化,只针对部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }

}

DataSourceType,枚举类,用于选择特定的数据源

/**
 * 枚举类,用于选择特定的数据源
 */
public enum DataSourceType {

    DB1, DB2

}

第六步,创建Task,User,如下

Task

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;

/**
 * <p>
 * 
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class Task implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键id
     */
    private Integer id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 邮箱
     */
    private String email;

}

User

import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
 * <p>
 * 
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键id
     */
    private Integer id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 年龄
     */
    private Integer age;

    /**
     * 邮箱
     */
    private String email;

}

第七步,分别创建UserMapper,UserMapper.xml,TaskMapper,TaskMapper.xml,如下

UserMapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luoyu.staticmultipledatasources.entity.User;
import org.apache.ibatis.annotations.Param;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
public interface UserMapper extends BaseMapper<User> {

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    User selectById(Integer id);

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @param name 姓名
     * @Date: 2020/2/13 下午12:06
     * @Return: int
     * @Throws:
     */
    int updateName(@Param("id") int id, @Param("name") String name);

}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luoyu.dynamicmultipledatasources.mapper.UserMapper">

    <select id="selectById" parameterType="java.lang.Integer" resultType="com.luoyu.dynamicmultipledatasources.entity.User">
        select * from user where id = #{id};
    </select>

    <update id="updateName">
        update user set name = #{name} where id = #{id};
    </update>

</mapper>

TaskMapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.luoyu.staticmultipledatasources.entity.Task;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
@Mapper
public interface TaskMapper extends BaseMapper<Task> {

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    Task selectById(Integer id);

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @param name 姓名
     * @Date: 2020/2/13 下午12:06
     * @Return: int
     * @Throws:
     */
    int updateName(@Param("id") int id, @Param("name") String name);

}

TaskMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.luoyu.dynamicmultipledatasources.mapper.TaskMapper">

    <select id="selectById" parameterType="java.lang.Integer" resultType="com.luoyu.dynamicmultipledatasources.entity.Task">
        select * from task where id = #{id};
    </select>

    <update id="updateName">
        update task set name = #{name} where id = #{id};
    </update>
    
</mapper>

第八步,创建自定义注解,AOP切面ChangeDataSource,ChangeDataSourceAspect,如下

ChangeDataSource

import java.lang.annotation.*;

/**
 * 切换数据注解 可以用于类或者方法级别 方法级别优先级 > 类级别
 */
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ChangeDataSource {

    //该值即key值,默认使用默认数据库
    String value() default "db1";

}

ChangeDataSourceAspect

import com.luoyu.dynamicmultipledatasources.config.DataSourceContextHolder;
import com.luoyu.dynamicmultipledatasources.enums.DataSourceType;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(-1)
public class ChangeDataSourceAspect {

    /**
     * 切换数据源
     */
    @Before("@annotation(changeDataSource)")
    public void changeDataSourceType(JoinPoint point, ChangeDataSource changeDataSource) throws Throwable {
        String value = changeDataSource.value();
        if (value.equals("db1")){
            DataSourceContextHolder.setDataSourceType(DataSourceType.DB1);
        }else if (value.equals("db2")){
            DataSourceContextHolder.setDataSourceType(DataSourceType.DB2);
        }else {
            // 默认使用db1
            DataSourceContextHolder.setDataSourceType(DataSourceType.DB1);
        }

    }

    /**
     * 清除数据源
     */
    @After("@annotation(changeDataSource)")
    public void clearDataSourceType(JoinPoint point, ChangeDataSource changeDataSource) {
        DataSourceContextHolder.clearDataSourceType();
    }

}

第九步,创建IUserService,UserServiceImpl,ITaskService,TaskServiceImpl,为了实现更优雅的动态数据源的切换,使用AOP+自定义注解的方式实现对方法级别的数据源切换。因为注解最低只能定义在方法上(而非代码块上),所以此种方式最细粒度为方法级别,99.99%情况下都够用了,如下

使用方法只需要在Service类的方法加上@ChangeDataSource("db2")注解,指定db2数据源即可,没有添加的话,默认db1数据源

IUserService

import com.baomidou.mybatisplus.extension.service.IService;
import com.luoyu.dynamicmultipledatasources.entity.User;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
public interface IUserService extends IService<User> {

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    User selectById(Integer id);

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @param name 姓名
     * @Date: 2020/2/13 下午12:06
     * @Return: int
     * @Throws:
     */
    int updateName(int id, String name);

}

UserServiceImpl

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luoyu.dynamicmultipledatasources.entity.User;
import com.luoyu.dynamicmultipledatasources.mapper.UserMapper;
import com.luoyu.dynamicmultipledatasources.service.IUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private UserMapper userMapper;

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    @Override
    public User selectById(Integer id) {
        return userMapper.selectById(id);
    }

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @param name 姓名
     * @Date: 2020/2/13 下午12:06
     * @Return: int
     * @Throws:
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int updateName(int id, String name) {
        int count = userMapper.updateName(id, name);
        // 此处报错事务回滚
        int i = 1/0;
        return count;
    }

}

ITaskService

import com.baomidou.mybatisplus.extension.service.IService;
import com.luoyu.dynamicmultipledatasources.entity.Task;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
public interface ITaskService extends IService<Task> {

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    Task selectById(Integer id);

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @param name 姓名
     * @Date: 2020/2/13 下午12:06
     * @Return: int
     * @Throws:
     */
    int updateName(int id, String name);

}

TaskServiceImpl

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luoyu.dynamicmultipledatasources.aop.ChangeDataSource;
import com.luoyu.dynamicmultipledatasources.entity.Task;
import com.luoyu.dynamicmultipledatasources.mapper.TaskMapper;
import com.luoyu.dynamicmultipledatasources.service.ITaskService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
@Service
public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task> implements ITaskService {

    @Resource
    private TaskMapper taskMapper;

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    @ChangeDataSource("db2")
    @Override
    public Task selectById(Integer id) {
        return taskMapper.selectById(id);
    }

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @param name 姓名
     * @Date: 2020/2/13 下午12:06
     * @Return: int
     * @Throws:
     */
    @Transactional(rollbackFor = Exception.class)
    @ChangeDataSource("db2")
    @Override
    public int updateName(int id, String name) {
        int count = taskMapper.updateName(id, name);
        int i = 1/0;
        return count;
    }
    
}

第十步,创建TaskController,UserController,如下

TaskController

import com.luoyu.dynamicmultipledatasources.entity.Task;
import com.luoyu.dynamicmultipledatasources.service.ITaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
@RestController
public class TaskController {

    @Autowired
    private ITaskService iTaskService;

    /**
     * 查询db2
     */
    @GetMapping(value = "/task")
    public Task getTaskById(@RequestParam("id") Integer id) {
        return iTaskService.selectById(id);
    }

    /**
     * 测试事物
     */
    @PutMapping(value = "/task")
    public int updateName(@RequestParam("id") Integer id, @RequestParam("name") String name) {
        return iTaskService.updateName(id, name);

    }

}

UserController

import com.luoyu.dynamicmultipledatasources.entity.User;
import com.luoyu.dynamicmultipledatasources.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author luoyu
 * @since 2020-02-13
 */
@RestController
public class UserController {

    @Autowired
    private IUserService iUserService;

    /**
     * 查询db1
     */
    @GetMapping(value = "/user")
    public User getUserById(@RequestParam("id") Integer id) {
        return iUserService.selectById(id);
    }

    /**
     * 测试事物
     */
    @PutMapping(value = "/user")
    public int updateName(@RequestParam("id") Integer id, @RequestParam("name") String name) {
        return iUserService.updateName(id, name);

    }

}

第十一步,启动项目,使用postman进行测试,如下

测试查db1,使用GET请求http://localhost:8189/user?id=2

image.png

测试db1事务,修改报错后再查db1,发现事务回滚了,使用PUT请求http://localhost:8189/user?id=2&name=test999

image.png

测试查db2,使用GET请求http://localhost:8189/task?id=2

image.png

测试db2事务,修改报错后再查db2,发现事务回滚了,使用PUT请求http://localhost:8189/task?id=2&name=test999

image.png

第十二步,有时候需要内部方法调用,这个时候直接调用的话AOP是会失效的,如下

说明

  1. 这个地方跟spring的@Transactional事务失效原因一致,因为都是基于AOP动态代理的模式,所以都可以用以下方法解决问题。
  2. 原因在于进行内部方法调用的时候没用到代理类,而是直接使用当前实例去进行自身调用,所以不会触发AOP切面。
  3. 解决思路很简单,只要我们在进行内部方法调用的时候,手动去spring容器拿到当前类的代理类进行调用,这个时候就可以触发AOP切面类。步骤如下

第一步,创建SpringUtils工具类,如下

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

@Component
public class SpringUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringUtils.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> cla) {
        return applicationContext.getBean(cla);
    }

    public static <T> T getBean(String name, Class<T> cal) {
        return applicationContext.getBean(name, cal);
    }

    public static Object getBean(String name){
        return applicationContext.getBean(name);
    }

    public static String getProperty(String key) {
        return applicationContext.getBean(Environment.class).getProperty(key);
    }

}

第二步,在TaskController,ITaskService,TaskServiceImpl类中新增测试方法,如下

TaskController

import com.luoyu.dynamicmultipledatasources.entity.Task;
import com.luoyu.dynamicmultipledatasources.service.ITaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
@RestController
public class TaskController {

    @Autowired
    private ITaskService iTaskService;

    /**
     * 查询db2
     */
    @GetMapping(value = "/task")
    public Task getTaskById(@RequestParam("id") Integer id) {
        return iTaskService.selectById(id);
    }

    /**
     * 测试事物
     */
    @PutMapping(value = "/task")
    public int updateName(@RequestParam("id") Integer id, @RequestParam("name") String name) {
        return iTaskService.updateName(id, name);

    }

    /**
     * 测试解决内部方法调用AOP失效问题
     */
    @GetMapping(value = "/taskByInside")
    public Task getTaskByInside(@RequestParam("id") Integer id) {
        return iTaskService.selectByInside(id);
    }

}

ITaskService

import com.baomidou.mybatisplus.extension.service.IService;
import com.luoyu.dynamicmultipledatasources.entity.Task;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
public interface ITaskService extends IService<Task> {

    /**
     * @Author: jinhaoxun
     * @Description: 解决内部方法调用AOP失效问题
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    Task selectByInside(Integer id);

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    Task selectById(Integer id);

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @param name 姓名
     * @Date: 2020/2/13 下午12:06
     * @Return: int
     * @Throws:
     */
    int updateName(int id, String name);

}

TaskServiceImpl

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luoyu.dynamicmultipledatasources.aop.ChangeDataSource;
import com.luoyu.dynamicmultipledatasources.entity.Task;
import com.luoyu.dynamicmultipledatasources.mapper.TaskMapper;
import com.luoyu.dynamicmultipledatasources.service.ITaskService;
import com.luoyu.dynamicmultipledatasources.util.SpringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author jinhaoxun
 * @since 2020-02-13
 */
@Service
public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task> implements ITaskService {

    @Resource
    private TaskMapper taskMapper;

    /**
     * @Author: jinhaoxun
     * @Description: 解决内部方法调用AOP失效问题
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    @Override
    public Task selectByInside(Integer id) {
        return this.getProxy().selectById(id);
    }

    /**
     * @Author: jinhaoxun
     * @Description: 从spring容器里手动拿到AOP代理类,解决AOP失效问题
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    private TaskServiceImpl getProxy(){
        return SpringUtils.getBean(this.getClass());
    }

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @Date: 2020/2/13 下午12:06
     * @Throws:
     */
    @ChangeDataSource("db2")
    @Override
    public Task selectById(Integer id) {
        return taskMapper.selectById(id);
    }

    /**
     * @Author: jinhaoxun
     * @Description:
     * @param id id
     * @param name 姓名
     * @Date: 2020/2/13 下午12:06
     * @Return: int
     * @Throws:
     */
    @Transactional(rollbackFor = Exception.class)
    @ChangeDataSource("db2")
    @Override
    public int updateName(int id, String name) {
        int count = taskMapper.updateName(id, name);
        int i = 1/0;
        return count;
    }

}

第三步,测试解决内部方法调用AOP失效问题,使用GET请求http://localhost:8189/taskByInside?id=2

image.png

完整代码地址:https://github.com/Jinhx128/springboot-demo

注:此工程包含多个module,本文所用代码均在dynamicmultipledatasources-demo模块下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容