SpringBoot高级进阶

数据源自动配置剖析

数据源配置方式

首先我们需要选择数据库驱动的库文件

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.33</version>
</dependency>

配置数据库连接

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3307/springboot
    username: root
    password: 123456

配置spring-boot-start-jdbc

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

编写测试类

    @Autowired
    private DataSource dataSource;

    @Test
    public void contextLoads()throws SQLException{
        Connection connection =dataSource.getConnection();
        System.out.println(connection);

    }

连接池配置方式

SpringBoot提供了三种连接池:

  • HikariCP
  • Commons DBCP2
  • Tomcat JDBC Connection Pool

其中SpringBoot默认使用HikariCP

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

要使用其他两个要改为

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-dbcp2</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
  <exclusions>
    <exclusion>
      <groupId>com.zaxxer</groupId>
      <artifactId>HikariCP</artifactId>
    </exclusion>
  </exclusions>
</dependency>
<dependency>
  <groupId>org.apache.tomcat</groupId>
  <artifactId>tomcat-jdbc</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jdbc</artifactId>
  <exclusions>
    <exclusion>
      <groupId>com.zaxxer</groupId>
      <artifactId>HikariCP</artifactId>
    </exclusion>
  </exclusions>
</dependency>

数据库自动配置

为什么前面我们说SpringBoot默认使用HikariCP?这是在哪里指定的?

首先我们需要找到DataSourceAutoConfiguration这个类

@Conditional(PooledDataSourceCondition.class)这个告诉我们指定的配置文件中,必须要有type属性。

现在我们进入DataSourceConfiguration这个里面

里面配置类SpringBoot启动的连接池。

private static final String[] DATA_SOURCE_TYPE_NAMES = new String[]
{"com.zaxxer.hikari.HikariDataSource",
"org.apache.tomcat.jdbc.pool.DataSource",
"org.apache.commons.dbcp2.BasicDataSource"};

所以我们可以看出来在没有指定Type时,默认就是HikariDataSource。

Druid连接池的配置

首先么要整合druid数据源

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>

在application.yml中引入druid的相关配置

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3307/springboot
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource

整合的配置类

public class DruidConfig {

    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }
}

SpringBoot整合Mybatis

首先我们要添加maven依赖

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

所以我们需要打开MybatisAutoConfiguration这个类

Mybatis自动配置源码剖析

  1. springBoot项目最核心的就是自动加载配置,该功能依赖的是一个注解@SpringBootApplication中的@EnableAutoConfiguration
  2. @EnableAutoConfiguration主要是通过@Import(AutoConfigurationImportSelector.class)类来加载的;
  3. MybatisAutoConfiguration
    1. 其中有个MybatisProperties,该类是对应mybatis的配置文件
    2. 有个SqlSessionFactory方法,作用是创建SqlSession类,Configuration类(mybatis最主要的类,保存着与mybatis相关的东西)
    3. SqlSessionTemplate,作用是mapperProoxy代理类有关

然后我们要看MapperScan注解

通过进一步分析源码我们可以得出以下结论

@MapperScan这个定义,是扫描指定包下面的mapper接口,然后设置每个mapper接口的beanClass属性为MapperFactoryBean类型并加入到spring的bean容器中。

MapperFactoryBean实现了FactoryBean接口,所以当spring从待实例化的bean容器中变量到这个bean并开始执行实例化使返回的对象实际上就是getObject方法中返回的对象。

最后我们看一下MapperFactoryBean的getObject方法,实际上返回的就是mybatis中通过getMapper拿到的对象,熟悉mybatis源码的就应该清楚,这个就是mybatis通过动态代理生成的mapper接口实现类。

到此,mapper接口现在也通过动态代理生成了实现类,并且注入到spring的bean容器中了,之后使用者就可以通过@Autowired或者getBean等方法,从spring容器中获取到了。

Spring+Mybatis实现动态数据源切换

动态数据源介绍

现在我们的项目中订单模块氛围正向和逆向两个部分;

  • 正向模块中记录了订单的基本信息,包括订单基本信息,订单商品信息,优惠卷信息,发票信息,账期信息,结算信息,订单备注信息,收货人信息等;
  • 逆向模块主要包含了商品退货信息和维修信息;
  • 当我们数据量超过500万时就需要考虑分库分表和读写分类,需要动态切换到对应的数据库中。

解决思路

Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置为一个Map,然后根据不同的key返回不同的数据源。

编码实战

首先我们需要准备基础的环境

@Data
public class Product {

    private Integer id ;
    private String name;
    private Double price;
}
@Mapper
public interface ProductMapper {

    //查询主数据库
    @Select("select * from product")
    public List<Product> getAllProductsM();

    //查询从数据库
    @Select("select * from product")
    public List<Product> getAllProductsS();
}
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductMapper productMapper;

    @Override
    public void getAllProductsM() {
        List<Product> products = productMapper.getAllProductsM();
        System.out.println("MyBatis查询结果:"+products);
    }

    @Override
    public void getAllProductsS() {
        List<Product> products = productMapper.getAllProductsS();
        System.out.println("MyBatis查询结果:"+products);
    }
}
@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/getAllProductsM")
    public String getAllProductsM() {
        productService.getAllProductsM();
        return "master";
    }

    @GetMapping("/getAllProductsS")
    public String getAllProductsS() {
        productService.getAllProductsS();
        return "slave";
    }
}

配置类

@Configuration
@Slf4j
public class MyDataSourceConfiguration {

    @Bean(name = "dataSourceM")
    DataSource dataSourceM(){
        log.info("主数据库");
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "dataSourceS")
    DataSource dataSourceS(){
        log.info("从数据库");
        return DataSourceBuilder.create().build();
    }
}

配置文件

spring:
  druid:
    datasource:
      master:
        url: jdbc:mysql://localhost:3307/product_m
        username: root
        password: 123456
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://localhost:3307/product_s
        username: root
        password: 123456
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver

其次需要编写RoutingDataSource,把两个真实的数据源代理为一个动态数据源:

@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return "master数据库";
    }

    @Bean
    @Primary
    DataSource primaryDataSource(@Autowired @Qualifier("dataSourceM")DataSource dataSourceM,
                                 @Autowired @Qualifier("dataSourceS")DataSource dataSourceS){

        log.info("正在动态创建数据库");
        Map<Object, Object> map = new HashMap<>();
        map.put("master数据库", dataSourceM);
        map.put("slave数据库", dataSourceS);
        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setTargetDataSources(map);
        routingDataSource.setDefaultTargetDataSource(dataSourceM);
        return routingDataSource;
    }
}

现在RoutingDataSource配置好了,但是路由的选择是写死的,永远返回master数据库。

现在问题来了,我们该如何存储动态选择的key已经在哪里设置key?

使用ThreadLocal存储key最合适。

public class RoutingDataSourceContext {
    
    static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();
    
    public static String getDataSourceKey() {
        String key = threadLocalDataSourceKey.get();
        return key == null ? "master数据库" : key;
    }
    
    public RoutingDataSourceContext(String key) {
        threadLocalDataSourceKey.set(key);
    }
    
    public  void close() {
        threadLocalDataSourceKey.remove();
    }
}

现在我们修改RoutingDataSource,获取key的代码

    @Override
    protected Object determineCurrentLookupKey() {
        return RoutingDataSourceContext.getDataSourceKey() ;
    }
@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/getAllProductsM")
    public String getAllProductsM() {
        RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext("master数据库");
        productService.getAllProductsM();
        return "master";
    }

    @GetMapping("/getAllProductsS")
    public String getAllProductsS() {
        RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext("salve数据库");
        productService.getAllProductsS();
        return "slave";
    }
}

测试结果

2025-12-12 14:07:48.541 [http-nio-8080-exec-3] DEBUG com.guslegend.mapper.ProductMapper.getAllProductsS - ==>  Preparing: select * from product 
2025-12-12 14:07:48.541 [http-nio-8080-exec-3] DEBUG com.guslegend.mapper.ProductMapper.getAllProductsS - ==> Parameters: 
2025-12-12 14:07:48.543 [http-nio-8080-exec-3] DEBUG com.guslegend.mapper.ProductMapper.getAllProductsS - <==      Total: 1
MyBatis查询结果:[Product(id=1, name=桌子, price=100.0)]
2025-12-12 14:07:50.442 [http-nio-8080-exec-4] DEBUG com.guslegend.mapper.ProductMapper.getAllProductsM - ==>  Preparing: select * from product 
2025-12-12 14:07:50.442 [http-nio-8080-exec-4] DEBUG com.guslegend.mapper.ProductMapper.getAllProductsM - ==> Parameters: 
2025-12-12 14:07:50.443 [http-nio-8080-exec-4] DEBUG com.guslegend.mapper.ProductMapper.getAllProductsM - <==      Total: 1
MyBatis查询结果:[Product(id=1, name=桌子, price=100.0)]

优化

上面需要读取数据库的地方就要加上这样的一段话,是否太过于复杂了?

RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext();

我们可以使用声明式事务管理,或者自定义注解来解决

首先我们要添加aop的依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

自定义注解


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingWith {
    
    String value() default "master";
}

切面类

@Aspect
@Component
public class RoutingAspect {
    
    @Around("@annotation(routingWith)")
    public Object routingWithDataSource(ProceedingJoinPoint proceedingJoinPoint,RoutingWith routingWith)throws Throwable{ 
        String key =routingWith.value();
        RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
        return proceedingJoinPoint.proceed();
    }
    
}

改造controller方法

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @RoutingWith("master")
    @GetMapping("/getAllProductsM")
    public String getAllProductsM() {
//        RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext("master数据库");
        productService.getAllProductsM();
        return "master";
    }

    @RoutingWith("slave")
    @GetMapping("/getAllProductsS")
    public String getAllProductsS() {
//        RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext("salve数据库");
        productService.getAllProductsS();
        return "slave";
    }
}

至此,我们实现了注解动态选择数据源功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值