• Spring Boot配置多数据源的四种方式


    1、导读

    在日常开发中我们都是以单个数据库进行开发,在小型项目中是完全能够满足需求的。
    但是,当我们牵扯到像淘宝、京东这样的大型项目的时候,单个数据库就难以承受用户的CRUD操作。
    那么此时,我们就需要使用多个数据源进行读写分离的操作,这种方式也是目前一种流行的数据管理方式。

    2、所需的资源

    1. Spring boot
    2. Mybatis-plus
    3. Alibab Druid数据库连接池
    4. MySql 数据库

    3、Spring Boot配置多数据源

    数据库

    在这里插入图片描述

    在YAML文件中定义数据源所需的数据

    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource ## 声明数据源的类型
        mysql-datasource1: ## 声明第一个数据源所需的数据
          url: jdbc:mysql://localhost:3306/mybatis?useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
    
        mysql-datasource2: ## 声明第二个数据源所需的数据
          url: jdbc:mysql://localhost:3306/bookstore?useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
    
        druid: ## druid数据库连接池的基本初始化属性
          initial-size: 5 ## 连接池初始化的大小
          min-idle: 1 ## 最小空闲的线程数
          max-active: 20 ## 最大活动的线程数
    
    
    mybatis-plus:
      mapper-locations: classpath:/mapper/*.xml ## 配置MyBatis-Plus扫描Mapper文件的位置
      type-aliases-package: com.example.sqlite.entity ## 创建别名的类所在的包
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    mysql-datasource1、mysql-datasource2是自定义的数据。

    定义多个数据源

    @Configuration
    public class DataSourceConfig {
    
        @Bean(name = "mysqlDataSource1")
        @ConfigurationProperties(prefix = "spring.datasource.mysql-datasource1")
        public DataSource dataSource1(){
            DruidDataSource build = DruidDataSourceBuilder.create().build();
            return build;
        }
    
    
        @Bean(name = "mysqlDataSource2")
        @ConfigurationProperties(prefix = "spring.datasource.mysql-datasource2")
        public DataSource dataSource2(){
            DruidDataSource build = DruidDataSourceBuilder.create().build();
            return build;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    @ConfigurationProperties注解用于将YAML中指定的数据创建成指定的对象,但是,YAML中的数据必须要与对象对象中的属性同名,不然无法由Spring Boot完成赋值。

    由于我们要定义多个数据源,所以在Spring Boot数据源自动配置类中就无法确定导入哪个数据源来完成初始化,所以我们就需要禁用掉Spring Boot的数据源自动配置类,然后使用我们自定义的数据源配置类来完成数据源的初始化与管理。

    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    public class DatasourceDomeApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DatasourceDomeApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在启动类上声明需要禁用的自动配置类:exclude = {DataSourceAutoConfiguration.class}

    3.1、实现DataSource接口

    缺点:产生大量的代码冗余,在代码中存在硬编码。

    3.1.1、代码

    @Component
    @Primary
    public class DynamicDataSource implements DataSource {
    
    //使用ThreadLocal而不是String,可以在多线程的时候保证数据的可靠性
        public static ThreadLocal<String> flag = new ThreadLocal<>();
    
        @Resource
        private DataSource mysqlDataSource1; // 注入第一个数据源
    
        @Resource
        private DataSource mysqlDataSource2; // 注入第二个数据源
    
    
        public DynamicDataSource(){ // 使用构造方法初始化ThreadLocal的值
            flag.set("r");
        }
    
        @Override
        public Connection getConnection() throws SQLException {
        	// 通过修改ThreadLocal来修改数据源,
        	// 为什么通过修改状态就能改变已经注入的数据源? 这就得看源码了。
            if(flag.get().equals("r")){ 
                return mysqlDataSource1.getConnection();
            } 
            return mysqlDataSource2.getConnection();
        }
    
        @Override
        public Connection getConnection(String username, String password) throws SQLException {
            return null;
        }
    
        @Override
        public PrintWriter getLogWriter() throws SQLException {
            return null;
        }
    
        @Override
        public void setLogWriter(PrintWriter out) throws SQLException {
    
        }
    
        @Override
        public void setLoginTimeout(int seconds) throws SQLException {
    
        }
    
        @Override
        public int getLoginTimeout() throws SQLException {
            return 0;
        }
    
        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    
        @Override
        public <T> T unwrap(Class<T> iface) throws SQLException {
            return null;
        }
    
        @Override
        public boolean isWrapperFor(Class<?> iface) throws SQLException {
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    实现DataSource接口我们本质上只使用了一个方法,就是getConnection()这个无参的方法,但是DataSource接口中所有的方法我们也都需要实现,只是不用写方法体而已,也就是存在了很多的 “废方法” 。
    @Primary注解 == @Order(1),用于设置此类的注入顺序。

    3.1.2、使用

    // 访问第一个数据库的t_user表
    
    @RestController
    public class UserController {
    
        @Resource
        private UserService userService;
    
        @GetMapping(value = "/user_list")
        public List<User> showUserList(){
            DynamicDataSource.flag.set("read"); // 修改数据源的状态
            List<User> list = userService.list();
            return list;
        }
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    // 访问第二个数据库的Book表
    
    @RestController
    public class BookController {
    
        @Resource
        private BookService BookService;
    
        @GetMapping(value = "/Book_list")
        public List<Book> getBookList(){
            DynamicDataSource.flag.set("write"); // 修改数据源的状态
            List<Book> list = BookService.list();
            return list;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3.2、继承AbstrictRoutingDataSource类

    减少了代码的冗余,但是还是会存在硬编码。

    3.2.1、代码

    @Primary
    @Component
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        public static ThreadLocal<String> flag = new ThreadLocal<>();
    
        @Resource
        private DataSource mysqlDataSource1;
    
        @Resource
        private DataSource mysqlDataSource2;
    
        public DynamicDataSource(){
            flag.set("read");
        }
    
        @Override
        protected Object determineCurrentLookupKey() { // 通过Key来得到数据源
            return flag.get();
        }
    
        @Override
        public void afterPropertiesSet() {
            Map<Object,Object> targetDataSource = new ConcurrentHashMap<>();
            targetDataSource.put("read",mysqlDataSource1);
            // 将第一个数据源设置为默认的数据源。
            super.setDefaultTargetDataSource(mysqlDataSource1);
            targetDataSource.put("write",mysqlDataSource2);
             // 将Map对象赋值给AbstrictRoutingDataSource内部的Map对象中。
            super.setTargetDataSources(targetDataSource);
            
            super.afterPropertiesSet();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    AbstrictRoutingDataSource的本质就是利用一个Map将数据源存储起来,然后通过Key来得到Value来修改数据源。

    3.2.2、使用

    // 访问第一个数据库的t_user表
    
    @RestController
    public class UserController {
    
        @Resource
        private UserService userService;
    
        @GetMapping(value = "/user_list")
        public List<User> showUserList(){
            DynamicDataSource.flag.set("read"); // 修改数据源的状态
            List<User> list = userService.list();
            return list;
        }
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    // 访问第二个数据库的Book表
    
    @RestController
    public class BookController {
    
        @Resource
        private BookService BookService;
    
        @GetMapping(value = "/Book_list")
        public List<Book> getBookList(){
            DynamicDataSource.flag.set("write"); // 修改数据源的状态
            List<Book> list = BookService.list();
            return list;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3.3、使用Spring AOP + 自定义注解的形式

    Spring AOP + 自定义注解的形式是一种推荐的写法,减少代码的冗余且不存在硬编码。
    此方法适合对指定功能操作指定数据库的模式。

    3.3.1、导入依赖

    <dependency>
       <groupId>org.springframework.bootgroupId>
       <artifactId>spring-boot-starter-aopartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    3.3.2、开启AOP支持

    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    @EnableAspectJAutoProxy //开启Spring Boot对AOP的支持
    public class AopDatasourceApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(AopDatasourceApplication.class, args);
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.3.3、定义枚举来表示数据源的标识

    public enum DataSourceType {
    
        MYSQL_DATASOURCE1,
    
        MYSQL_DATASOURCE2,
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.3.4、继承AbstractRoutingDataSource类

    @Primary
    @Component
    public class DataSourceManagement extends AbstractRoutingDataSource {
    
        public static ThreadLocal<String> flag = new ThreadLocal<>();
    
        @Resource
        private DataSource mysqlDataSource1;
    
        @Resource
        private DataSource mysqlDataSource2;
    
        public DataSourceManagement(){
            flag.set(DataSourceType.MYSQL_DATASOURCE1.name());
        }
    
        @Override
        protected Object determineCurrentLookupKey() {
            return flag.get();
        }
    
        @Override
        public void afterPropertiesSet() {
            Map<Object,Object> targetDataSource = new ConcurrentHashMap<>();
            targetDataSource.put(DataSourceType.MYSQL_DATASOURCE1.name(),mysqlDataSource1);
            targetDataSource.put(DataSourceType.MYSQL_DATASOURCE2.name(),mysqlDataSource2);
            super.setTargetDataSources(targetDataSource);
            super.setDefaultTargetDataSource(mysqlDataSource1);
            super.afterPropertiesSet();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    3.3.5、自定义注解

    @Target({ElementType.TYPE,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface TargetDataSource {
    
        DataSourceType value() default DataSourceType.MYSQL_DATASOURCE1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.3.6、定义注解的实现类

    @Component
    @Aspect
    @Slf4j
    public class TargetDataSourceAspect {
    
    
        @Before("@within(TargetDataSource) || @annotation(TargetDataSource)")
        public void beforeNoticeUpdateDataSource(JoinPoint joinPoint){
            TargetDataSource annotation = null;
            Class<? extends Object> target = joinPoint.getTarget().getClass();
            if(target.isAnnotationPresent(TargetDataSource.class)){
                // 判断类上是否标注着注解
                 annotation = target.getAnnotation(TargetDataSource.class);
                 log.info("类上标注了注解");
            }else{
                Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
                if(method.isAnnotationPresent(TargetDataSource.class)){
                    // 判断方法上是否标注着注解,如果类和方法上都没有标注,则报错
                    annotation = method.getAnnotation(TargetDataSource.class);
                    log.info("方法上标注了注解");
                }else{
                    throw new RuntimeException("@TargetDataSource注解只能用于类或者方法上, 错误出现在:[" +
                            target.toString() +" " + method.toString() + "];");
                }
            }
            // 切换数据源
            DataSourceManagement.flag.set(annotation.value().name());
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在有的博客中也会使用@Around环绕通知的方式,但是环绕通知需要执行joinPoint.process()方法来调用目标对象的方法,最后返回执行的值,不然得不到所需要的数据。
    我这里使用了@Before前置通知,效果是一样的,因为@Around就会包含@Before。

     @Around("@within(TargetDataSource) || @annotation(TargetDataSource)")
        public Object beforeNoticeUpdateDataSource(ProceedingJoinPoint joinPoint){
           	// 省略逻辑代码
           	Object result = null;
           	try {
                result = joinPoint.proceed();
            } catch (Throwable e) {
                e.printStackTrace();
            }
            return result;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    ProceedingJoinPoint 对象只能在@Around环绕通知中使用,在其他通知中使用就会报错。

    3.3.7、使用

    // 访问第一个数据源。
    
    @RestController
    // 将注解标注在类上,表示本类中所有的方法都是使用数据源1
    @TargetDataSource(value = DataSourceType.MYSQL_DATASOURCE1)
    public class UserController {
    
        @Resource
        private UserService userService;
    
        @GetMapping(value = "/user_list")
        public List<User> showUserList(){
            System.out.println(DataSourceType.MYSQL_DATASOURCE1.name());
            List<User> list = userService.list();
            return list;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    // 访问第二个数据源
    
    @RestController
    public class BookController {
    
        @Resource
        private BookService BookService;
    
        @GetMapping(value = "/Book_list")
        // 将注解标注在方法上,表示此方法使用数据源2
        @TargetDataSource(value = DataSourceType.MYSQL_DATASOURCE2)
        public List<Book> getBookList(){
            List<Book> list = BookService.list();
            return list;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3.4、通过SqlSessionFactory指定的数据源来操作指定目录的XML文件

    使用此方法则不会与上面所述的类有任何关系,本方法会重新定义类。
    本方法也是一种推荐的方法,适用于对指定数据库的操作,也就是适合读写分离。不会存在代码冗余和存在硬编码。

    3.4.1、项目的目录结构

    对所需要操作的数据库的Mapper层和dao层分别建立一个文件夹。

    在这里插入图片描述

    3.4.2、配置YAML文件

    spring:
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        mysql-datasource:
          jdbc-url: jdbc:mysql://localhost:3306/mybatis?useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
    
        sqlite-datasource:
          jdbc-url: jdbc:mysql://localhost:3306/bookstore?useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
    
        druid:
          initial-size: 5
          min-idle: 1
          max-active: 20
    
    
    mybatis-plus:
      mapper-locations: classpath:/mapper/*.xml
      type-aliases-package: com.example.sqlite.entity
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    3.4.3、针对Mapper层通过SqlSessionFactory指定数据源来操作

    3.4.3.1、创建MySql数据源
    
    @Configuration
    @MapperScan(basePackages = "com.example.sqlite.dao.mysql", sqlSessionFactoryRef = "MySQLSqlSessionFactory")
    public class MySQLDataSourceConfig {
    
        @Bean(name = "MySQLDataSource")
        @Primary
        @ConfigurationProperties(prefix = "spring.datasource.mysql-datasource")
        public DataSource getDateSource1() {
            return DataSourceBuilder.create().build();
        }
    
    
        @Bean(name = "MySQLSqlSessionFactory")
        @Primary
        public SqlSessionFactory test1SqlSessionFactory(
                @Qualifier("MySQLDataSource") DataSource datasource) throws Exception {
            MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean ();
            bean.setDataSource(datasource);
            bean.setMapperLocations(// 设置mybatis的xml所在位置
                    new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/mysql/*.xml"));
            return bean.getObject();
        }
    
    
        @Bean("MySQLSqlSessionTemplate")
        @Primary
        public SqlSessionTemplate test1SqlSessionTemplate(
                @Qualifier("MySQLSqlSessionFactory") SqlSessionFactory sessionFactory) {
            return new SqlSessionTemplate(sessionFactory);
        }
    
        @Bean
        public PlatformTransactionManager transactionManager(@Qualifier("MySQLDataSource")DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    3.4.3.2、创建Sqlite数据源
    @Configuration
    @MapperScan(basePackages = "com.example.sqlite.dao.sqlite", sqlSessionFactoryRef = "SqliteSqlSessionFactory")
    public class SqliteDataSourceConfig {
    
        @Bean(name = "SqliteDateSource")
        @ConfigurationProperties(prefix = "spring.datasource.sqlite-datasource")
        public DataSource getDateSource1() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean(name = "SqliteSqlSessionFactory")
        public SqlSessionFactory test1SqlSessionFactory(
                @Qualifier("SqliteDateSource") DataSource datasource) throws Exception {
            MybatisSqlSessionFactoryBean  bean = new MybatisSqlSessionFactoryBean();
            bean.setDataSource(datasource);
            bean.setMapperLocations(
                    new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/sqlite/*.xml"));
            return bean.getObject();
        }
    
        @Bean("SqliteSqlSessionTemplate")
        public SqlSessionTemplate test1SqlSessionTemplate(
                @Qualifier("SqliteSqlSessionFactory") SqlSessionFactory sessionFactory) {
            return new SqlSessionTemplate(sessionFactory);
        }
    
        @Bean
        public PlatformTransactionManager transactionManager(@Qualifier("SqliteDateSource")DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    1. @MapperScan注解中的basePackages指向的是指定的Dao层。
    2. @MapperScan注解中sqlSessionFactoryRef 用来指定使用某个SqlSessionFactory来操作数据源。
    3. bean.setMapperLocations(
      new PathMatchingResourcePatternResolver()
      .getResources(“classpath*:mapper/sqlite/*.xml”)); 指向的是操作执行数据库的Mapper层。

    如果使用SQLite数据库,那么就必须在项目中内嵌SQLite数据库,这个一个轻量级的数据库,不同于Mysql,SQLite不需要服务器,SQLite适合使用于移动APP开发。
    像微信,用户的聊天记录就是使用这个数据库进行存储。SQLite也可以使用在Web端,只是不太方便。

    3.4.4、使用

    // 访问第一个数据库
    
    @RestController
    public class UserController {
    
        @Resource
        private UserService userService;
    
        @GetMapping(value = "/user_list")
        public List<User> showUserList(){
            List<User> list = userService.list();
            return list;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    // 访问第二个数据库
    
    @RestController
    public class AddressController {
    
        @Resource
        private AddressService addressService;
    
        @GetMapping(value = "/address_list")
        public List<Address> getAddressList(){
            List<Address> list = addressService.list();
            return list;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    使用此种方法不会存在任何代码的冗余以及硬编码的存在,但是需要分层明确。
    唯一的不足就是添加一个数据源就需要重新写一个类,而这个类中的代码大部分又是相同的。

    4、总结

    1. 实现DataSource接口这种写法是不推荐的。
    2. 推荐使用Spring Boot + 自定义注解的方式与SqlSessionFactory方式。

    另外,Spring AOP中各种通知的执行顺序如下图所示:在这里插入图片描述

  • 相关阅读:
    python
    Docker启动mysql服务
    LeetCode每日一题——902. 最大为 N 的数字组合
    【MySQL】 Linux平台MySQL安装
    阿里云国际版回执消息简介与配置流程
    uniapp开发h5 调用微信sdk 全网最全指南!!!! 血泪史!!!
    Mybatis
    《文献阅读》- 遗传算法作为量子近似优化算法的经典优化器(未完成)
    排序-选择类排序
    深入理解比特币原理1----常用术语与概念介绍
  • 原文地址:https://blog.csdn.net/qq_45515182/article/details/126330084