应用场景:
对于数据量在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
水平切分图,数据入不同的库中
实现图:
- 图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
,来判断你的项目是否需要添加了webmvc
和tomcat
,就会自动的帮你配置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"})