定制网站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是自定义的数据。

定义多个数据源

@Configurationpublic 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@Primarypublic 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表@RestControllerpublic 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表@RestControllerpublic 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@Componentpublic 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表@RestControllerpublic 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表@RestControllerpublic 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 + 自定义注解的形式

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

3.3.1、导入依赖

<dependency>   <groupId>org.springframework.boot</groupId>   <artifactId>spring-boot-starter-aop</artifactId></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@Componentpublic 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)@Documentedpublic @interface TargetDataSource {    DataSourceType value() default DataSourceType.MYSQL_DATASOURCE1;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3.3.6、定义注解的实现类

@Component@Aspect@Slf4jpublic 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
// 访问第二个数据源@RestControllerpublic 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: 20mybatis-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、使用

// 访问第一个数据库@RestControllerpublic 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
// 访问第二个数据库@RestControllerpublic 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中各种通知的执行顺序如下图所示:

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发