应用场景:

       对于数据量在1千万,单个mysql数据库就可以支持,但是如果数据量大于这个数时,那么查询的性能就会很低或是两个不同的数据库时。此时需要对数据库做水平切分,常见的做法是按照用户的账号进行hash,然后选择对应的数据库,以下是在springboot项目中利用AOP面向切面技术实现两个不同数据库之间的来回切换功能

一 配置数据源连接池 

application.yml或application.properties
bootdo:
   uploadPath: /var/uploaded_files/
   httpPath: http://os.suyongw.com
 logging:
   level:
     root: info
     com.bootdo: debug
 spring:
   datasource:
     type: com.alibaba.druid.pool.DruidDataSource
     primary:   注:主数据源连接池
       driverClassName: com.mysql.jdbc.Driver
       url: jdbc:mysql://localhost:3306/bootdo?useUnicode=true&characterEncoding=utf8
       username: 
       password: 
       initialSize: 1
       minIdle: 3
       maxActive: 20
       # 配置获取连接等待超时的时间
       maxWait: 60000
       # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
       timeBetweenEvictionRunsMillis: 60000
       # 配置一个连接在池中最小生存的时间,单位是毫秒
       minEvictableIdleTimeMillis: 30000
       validationQuery: select 'x'
       testWhileIdle: true
       testOnBorrow: false
       testOnReturn: false
       # 打开PSCache,并且指定每个连接上PSCache的大小
       poolPreparedStatements: true
       maxPoolPreparedStatementPerConnectionSize: 20
       # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
       filters: stat,wall,slf4j
       # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
       connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
       # 合并多个DruidDataSource的监控数据
       #useGlobalDataSourceStat: true     secondary: 注:次数据源连接池
       driverClassName: com.mysql.jdbc.Driver
       url: jdbc:mysql://localhost:3306/suyong?useUnicode=true&characterEncoding=utf8
       username: 
       password: 
       initialSize: 1
       minIdle: 3
       maxActive: 20
       # 配置获取连接等待超时的时间
       maxWait: 60000
       # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
       timeBetweenEvictionRunsMillis: 60000
       # 配置一个连接在池中最小生存的时间,单位是毫秒
       minEvictableIdleTimeMillis: 30000
       validationQuery: select 'x'
       testWhileIdle: true
       testOnBorrow: false
       testOnReturn: false
       # 打开PSCache,并且指定每个连接上PSCache的大小
       poolPreparedStatements: true
       maxPoolPreparedStatementPerConnectionSize: 20
       # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
       filters: stat,wall,slf4j
       # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
       connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
       # 合并多个DruidDataSource的监控数据
       #useGlobalDataSourceStat: true
   redis:
       host: localhost
       port: 6379
       password:
       # 连接超时时间(毫秒)
       timeout: 10000
       pool:
         # 连接池中的最大空闲连接
         max-idle: 8
         # 连接池中的最小空闲连接
         min-idle: 10
         # 连接池最大连接数(使用负值表示没有限制)
         max-active: 100
         # 连接池最大阻塞等待时间(使用负值表示没有限制)
         max-wait: -1
         
 mybatis: org.mybatis.spring.SqlSessionFactoryBean

  水平切分图,数据入不同的库中

Java master切换数据库 spring切换数据库_数据源

实现图:

Java master切换数据库 spring切换数据库_spring_02

  • 图1是比较常见的情况,单个数据库
  • 图2展示了web应用和数据库之间的一个中间层,这个中间层去选择使用哪个数据库

代码实现

配置数据源类:

package com.bootdo.common.config;
import org.mybatis.spring.SqlSessionFactoryBean;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;  
 import org.springframework.boot.context.properties.ConfigurationProperties;  
 import org.springframework.context.annotation.Bean;  
 import org.springframework.context.annotation.Configuration;  
 import org.springframework.context.annotation.Primary;  
 import org.springframework.jdbc.datasource.DataSourceTransactionManager;  
 import org.springframework.transaction.PlatformTransactionManager;  
   
 import javax.sql.DataSource;  
 import java.util.HashMap;  
 import java.util.Map;  /**
  * @author Administrator
  *多数据源配置类
  */
 @Configuration
 public class DataSourceConfig {
     
     //数据源1  
     @Bean(name = "datasource1")  
     @ConfigurationProperties(prefix = "spring.datasource.primary") // application.properteis中对应属性的前缀  
     public DataSource dataSource1() {  
         return DataSourceBuilder.create().build();  
     }  
   
     //数据源2  
     @Bean(name = "datasource2")  
     @ConfigurationProperties(prefix = "spring.datasource.secondary") // application.properteis中对应属性的前缀  
     public DataSource dataSource2() {  
         return DataSourceBuilder.create().build();  
     }  
   
     /** 
      * 动态数据源: 通过AOP在不同数据源之间动态切换 
      * @return 
      */  
     @Primary  
     @Bean(name = "dynamicDataSource")  
     public DataSource dynamicDataSource() {  
         DynamicDataSource dynamicDataSource = new DynamicDataSource();  
         // 默认数据源  
         dynamicDataSource.setDefaultTargetDataSource(dataSource1());  
         // 配置多数据源  
         Map<Object, Object> dsMap = new HashMap<Object, Object>();  
         dsMap.put("datasource1", dataSource1());  
         dsMap.put("datasource2", dataSource2());  
   
         dynamicDataSource.setTargetDataSources(dsMap);  
         return dynamicDataSource;  
     }  
   
     /** 
      * 配置@Transactional注解事物 
      * @return 
      */  
     @Bean  
     public PlatformTransactionManager transactionManager() {  
         return new DataSourceTransactionManager(dynamicDataSource());  
     }  
 /*    
     @Bean
     @ConfigurationProperties(prefix = "mybatis")
     public SqlSessionFactoryBean sqlSessionFactoryBean() {        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // Here is very important, if don't config this, will can't switch datasource
        // put all datasource into SqlSessionFactoryBean, then will autoconfig SqlSessionFactory
        sqlSessionFactoryBean.setDataSource(dynamicDataSource());
        return sqlSessionFactoryBean;
    }*/
     
 } 
默认数据源类:
package com.bootdo.common.config;
public class DataSourceContextHolder {
     /** 
      * 默认数据源 
      */  
     public static final String DEFAULT_DS = "datasource1";  
   
     private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();  
   
     // 设置数据源名  
     public static void setDB(String dbType) {  
         System.out.println("切换到{"+dbType+"}数据源");  
         contextHolder.set(dbType);  
     }  
   
     // 获取数据源名  
     public static String getDB() {  
         return (contextHo  lder.get());  
     }  
   
     // 清除数据源名  
     public static void clearDB() {  
         contextHolder.remove();  
     }  
 }
  自定义AOP切面类+数据自动切换类:
package com.bootdo.common.config;
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.aspectj.lang.reflect.MethodSignature;  
 import org.springframework.stereotype.Component;import java.lang.reflect.Method; 
/**
  * @author Administrator
  *自定义注解 + aop方式实现数据源动态切换
  */
 @Aspect  
 @Component  
 public class DynamicDataSourceAspect {
       
     @Before("@annotation(DS)")  
     public void beforeSwitchDS(JoinPoint point){  
         //获得当前访问的class  
         Class<?> className = point.getTarget().getClass();  
         //获得访问的方法名  
         String methodName = point.getSignature().getName();  
         //得到方法的参数的类型  
         Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();  
         String dataSource = DataSourceContextHolder.DEFAULT_DS;  
         try {  
             // 得到访问的方法对象  
             Method method = className.getDeclaredMethod(methodName, argClass);  
             // 判断是否存在@DS注解  
             if (method.isAnnotationPresent(DS.class)) {  
                 DS annotation = method.getAnnotation(DS.class);  
                 // 取出注解中的数据源名  
                 dataSource = annotation.value();  
             }  
         } catch (Exception e) {  
             e.printStackTrace();  
         }  
         // 切换数据源  
         DataSourceContextHolder.setDB(dataSource);  
     }  
       
     @After("@annotation(DS)")  
     public void afterSwitchDS(JoinPoint point){  
         DataSourceContextHolder.clearDB();  
     }  
 }
  自定义类继承AbstractRoutingDataSource类,实现determineCurrentLookupKey()方法:
package com.bootdo.common.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource{
    @Override
     protected Object determineCurrentLookupKey() {
         //System.out.println("数据源为"+DataSourceContextHolder.getDB());  
         return DataSourceContextHolder.getDB();  
     }}

自定义AOP切面接口类:

package com.bootdo.common.config;
import java.lang.annotation.ElementType;  
 import java.lang.annotation.Retention;  
 import java.lang.annotation.RetentionPolicy;  
 import java.lang.annotation.Target;  /**
  * @author Administrator
  *自定义注解
  */
 @Retention(RetentionPolicy.RUNTIME)  
 @Target({ElementType.METHOD})   //用于描述方法
 public @interface DS {
     String value() default "datasource1"; 
 } 
主函数入口引用:
package com.bootdo;
import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
 import org.springframework.boot.web.servlet.ServletComponentScan;
 import org.springframework.transaction.annotation.EnableTransactionManagement;import com.alipay.demo.trade.config.Configs;
 @EnableTransactionManagement  // 启注解事务管理,等同于xml配置方式的 <tx:annotation-driven />@ServletComponentScan
 @MapperScan("com.bootdo.*.dao")
 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
 public class BootdoApplication {
     public static void main(String[] args) {
          SpringApplication.run(BootdoApplication.class, args);
          Configs.init("zfbinfo.properties");
          System.out.println("ヾ(◍°∇°◍)ノ゙  启动成功      ヾ(◍°∇°◍)ノ゙\n" );
     }
 }

上述代码中的注解说明:  

自定义接口类注解:

注解@Retention可以用来修饰注解,是注解的注解,称为元注解。
Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型,
这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention 搭配 RententionPolicy使用。RetentionPolicy有3个值:CLASS  RUNTIME   SOURCE
按生命周期来划分可分为3类:
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。
那怎么来选择合适的注解生命周期呢?
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。
一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解,比如@Deprecated使用RUNTIME注解
如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;
如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,使用SOURCE 注解。

注解@Override用在方法上,当我们想重写一个方法时,在方法上加@Override,当我们方法的名字出错时,编译器就会报错
注解@Deprecated,用来表示某个类或属性或方法已经过时,不想别人再用时,在属性和方法上用@Deprecated修饰
注解@SuppressWarnings用来压制程序中出来的警告,比如在没有用泛型或是方法已经过时的时候

@Target:

   @Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。

  作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

  取值(ElementType)有:

    1.CONSTRUCTOR:用于描述构造器
    2.FIELD:用于描述域
    3.LOCAL_VARIABLE:用于描述局部变量
    4.METHOD:用于描述方法
    5.PACKAGE:用于描述包
    6.PARAMETER:用于描述参数
    7.TYPE:用于描述类、接口(包括注解类型) 或enum声明

 

@EnableTransactionManagement 是Spring Boot 使用事务的注解 开启事务支持后,然后在访问数据库的Service方法上添加注解 @Transactional 便可
 
@ServletComponentScan 注解在 SpringBootApplication 上使用后,Servlet、Filter、Listener 可以直接通过 @WebServlet、@WebFilter、@WebListener 注解自动注册,无需其他代码。
Spring Boot MyBatis注解:@MapperScan和@Mapper
@MapperScan可以指定要扫描的Mapper类的包的路径 
 @MapperScan("com.bootdo.*.dao")  //代表扫描多个包
 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
 public class BootdoApplication {
     public static void main(String[] args) {
          SpringApplication.run(BootdoApplication.class, args);
          Configs.init("zfbinfo.properties");
          System.out.println("ヾ(◍°∇°◍)ノ゙  启动成功      ヾ(◍°∇°◍)ノ゙\n" );
     }
 }@Mapper的话需要每个mapper接口类中添加 麻烦
       @Mapper  
   public interface DemoMapper {  
          @Insert("insert into Demo(name) values(#{name})")  
          @Options(keyProperty="id",keyColumn="id",useGeneratedKeys=true)  
          public void save(Demo demo); 
 }

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) 注解的作用

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

该注解的作用是,排除自动注入数据源的配置(取消数据库配置),一般使用在客户端(消费者)服务中

 

@SpringBootApplication注解分析

首先我们分析的就是入口类Application的启动注解@SpringBootApplication,源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
....
}

发现@SpringBootApplication是一个复合注解,包括@ComponentScan,和@SpringBootConfiguration@EnableAutoConfiguration

  • @SpringBootConfiguration继承自@Configuration,二者功能也一致,标注当前类是配置类,并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到srping容器中,并且实例名就是方法名。
  • @EnableAutoConfiguration的作用启动自动的配置,@EnableAutoConfiguration注解的意思就是Springboot根据你添加的jar包来配置你项目的默认配置,比如根据spring-boot-starter-web ,来判断你的项目是否需要添加了webmvctomcat,就会自动的帮你配置web项目中所需要的默认配置。在下面博客会具体分析这个注解,快速入门的demo实际没有用到该注解。
  • @ComponentScan,扫描当前包及其子包下被@Component@Controller@Service@Repository注解标记的类并纳入到spring容器中进行管理。是以前的<context:component-scan>(以前使用在xml中使用的标签,用来扫描包配置的平行支持)。所以本demo中的User为何会被spring容器管理。

根据上面的理解,上面的入口类Application,可以使用:

package com.zhihao.miao;
import com.zhihao.miao.bean.User;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import java.util.Map;

@ComponentScan
public class Application {

    @Bean
    public Runnable createRunnable(){
        return () -> System.out.println("spring boot is running");
    }

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Application.class,args);
        context.getBean(Runnable.class).run();
        System.out.println(context.getBean(User.class));
        Map map = (Map) context.getBean("createMap");
        int age = (int) map.get("age");
        System.out.println("age=="+age);

    }
}

使用@ComponentScan注解代替@SpringBootApplication注解,也可以正常运行程序。原因是@SpringBootApplication中包含@ComponentScan,并且springboot会将入口类看作是一个@SpringBootConfiguration标记的配置类,所以定义在入口类Application中的Runnable也可以纳入到容器管理。

SpringBootApplication参数详解

图片.png

  • Class<?>[] exclude() default {}:
    根据class来排除,排除特定的类加入spring容器,传入参数value类型是class类型。
  • String[] excludeName() default {}:
    根据class name来排除,排除特定的类加入spring容器,传入参数value类型是class的全类名字符串数组。
  • String[] scanBasePackages() default {}:
    指定扫描包,参数是包名的字符串数组。
  • Class<?>[] scanBasePackageClasses() default {}:
    扫描特定的包,参数类似是Class类型数组。

在包下com.zhihao.miao.springboot定义一个启动应用类(加上@SpringBootApplication注解)

package com.zhihao.miao.springboot;


import com.zhihao.miao.beans.Cat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =SpringApplication.run(Application.class,args);
        Cat cat = context.getBean(Cat.class);
        System.out.println(cat);
    }
}

在com.zhihao.miao.beans包下定义一个实体类,并且想将其纳入到spring容器中,

public class Cat {
}
package com.zhihao.miao.beans;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    @Bean
    public Cat cat(){
        return new Cat();
    }

}

启动启动类,打印结果如下:

 说明Cat类并没有纳入到spring容器中,这个结果也如我们所想,因为@SpringBootApplication只会扫描@SpringBootApplication注解标记类包下及其子包的类(特定注解标记,比如说@Controller,@Service,@Component,@Configuration和@Bean注解等等)纳入到spring容器,很显然MyConfig不在@SpringBootApplication注解标记类相同包下及其子包的类,所以需要我们去配置一下扫包路径。

修改启动类,@SpringBootApplication(scanBasePackages = "com.zhihao.miao"),指定扫描路径:

package com.zhihao.miao.springboot;

import com.zhihao.miao.beans.Cat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication(scanBasePackages = "com.zhihao.miao")
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =SpringApplication.run(Application.class,args);
        Cat cat = context.getBean(Cat.class);
        System.out.println(cat);
    }
}

启动并打印:

 

当然使用@SpringBootApplication(scanBasePackageClasses = MyConfig.class),指定scanBasePackageClasses参数的value值是你需要扫描的类也可以,结果一样,不过如果多个配置类不在当前包及其子包下,则需要指定多个。

再看一个列子,
在上面的列子的相同包下(com.zhihao.miao.springboot)配置了People,并将其纳入到spring容器中(@Component),我们知道@SpringBootApplication注解会扫描当前包及其子包,所以People类会纳入到spring容器中去,我们需要将其排除在spring容器中,如何操作?
可以使用@SpringBootApplication的另外二个参数(exclude或excludeName)

package com.zhihao.miao.springboot;

import org.springframework.stereotype.Component;

@Component
public class People {
}

启动类,

package com.zhihao.miao.springboot;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =SpringApplication.run(Application.class,args);
        People people = context.getBean(People.class);
        System.out.println(people);
    }
}

启动并打印结果:

 然后修改@SpringBootApplication配置,

package com.zhihao.miao.springboot;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication(exclude = People.class)
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =SpringApplication.run(Application.class,args);
        People people = context.getBean(People.class);
        System.out.println(people);
    }
}

很明显启动报错。使用@excludeName注解也可以。如下,

@SpringBootApplication(excludeName = {"com.zhihao.miao.springboot.People"})