package com.example.dynamicdatasourcetest.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(LoadDataSource loadDataSource)
{
Map<String, DataSource> dataSourceMap = loadDataSource.loadAllDataSource();
super.setTargetDataSources(new HashMap<>(dataSourceMap));
super.setDefaultTargetDataSource(dataSourceMap.get(DataSourceConst.DEFAULT_DATASOURCE));
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
从上面核心步骤可以知道,我们现在还缺少数据源,所以接下来就是根据配置文件加载数据源。
这里要注意的是@EnableConfigurationProperties(DruidProperties.class)和@ConfigurationProperties(prefix = “spring.datasource”)的使用。前者是为了将DruidProperties注入到LoadDataSource中,后者是将application.yml中的配置读入。
package com.example.dynamicdatasourcetest.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.example.dynamicdatasourcetest.config.DruidProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Component
@EnableConfigurationProperties(DruidProperties.class)
public class LoadDataSource {
@Autowired
DruidProperties druidProperties;
public Map<String, DataSource> loadAllDataSource() {
Map<String, DataSource> map = new HashMap<>();
Map<String, Map<String, String>> ds = druidProperties.getDs();
try {
Set<String> keySet = ds.keySet();
for (String key : keySet) {
map.put(key, druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))));
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
package com.example.dynamicdatasourcetest.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {
private int initialSize;
private int minIdle;
private int maxActive;
private int maxWait;
private int timeBetweenEvictionRunsMillis;
private int minEvictableIdleTimeMillis;
private int maxEvictableIdleTimeMillis;
private String validationQuery;
private boolean testWhileIdle;
private boolean testOnBorrow;
private boolean testOnReturn;
private Map<String, Map<String,String>> ds;
public DruidDataSource dataSource(DruidDataSource datasource) {
/** 配置初始化大小、最小、最大 */
datasource.setInitialSize(initialSize);
datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle);
/** 配置获取连接等待超时的时间 */
datasource.setMaxWait(maxWait);
/** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
/** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
/**
* 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
*/
datasource.setValidationQuery(validationQuery);
/** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
datasource.setTestWhileIdle(testWhileIdle);
/** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
datasource.setTestOnBorrow(testOnBorrow);
/** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
datasource.setTestOnReturn(testOnReturn);
return datasource;
}
public Map<String, Map<String, String>> getDs() {
return ds;
}
public void setDs(Map<String, Map<String, String>> ds) {
this.ds = ds;
}
public int getInitialSize() {
return initialSize;
}
public void setInitialSize(int initialSize) {
this.initialSize = initialSize;
}
public int getMinIdle() {
return minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
public int getMaxActive() {
return maxActive;
}
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
public int getMaxWait() {
return maxWait;
}
public void setMaxWait(int maxWait) {
this.maxWait = maxWait;
}
public int getTimeBetweenEvictionRunsMillis() {
return timeBetweenEvictionRunsMillis;
}
public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) {
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
}
public int getMinEvictableIdleTimeMillis() {
return minEvictableIdleTimeMillis;
}
public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) {
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
}
public int getMaxEvictableIdleTimeMillis() {
return maxEvictableIdleTimeMillis;
}
public void setMaxEvictableIdleTimeMillis(int maxEvictableIdleTimeMillis) {
this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
}
public String getValidationQuery() {
return validationQuery;
}
public void setValidationQuery(String validationQuery) {
this.validationQuery = validationQuery;
}
public boolean isTestWhileIdle() {
return testWhileIdle;
}
public void setTestWhileIdle(boolean testWhileIdle) {
this.testWhileIdle = testWhileIdle;
}
public boolean isTestOnBorrow() {
return testOnBorrow;
}
public void setTestOnBorrow(boolean testOnBorrow) {
this.testOnBorrow = testOnBorrow;
}
public boolean isTestOnReturn() {
return testOnReturn;
}
public void setTestOnReturn(boolean testOnReturn) {
this.testOnReturn = testOnReturn;
}
}
第一步中的determineCurrentLookupKey方法中,我们是从DynamicDataSourceContextHolder中来获取数据源key,那么这个key是什么时候来的呢?为什么要用ThreadLocal保存?
package com.example.dynamicdatasourcetest.datasource;
public class DynamicDataSourceContextHolder {
private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dsType) {
CONTEXT_HOLDER.set(dsType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
这里就是一种简单的使用,根据业务需要更改。一般像一些多租户模式的项目,会将用户使用的数据源放在请求头中,后台用拦截器来切换数据源。
@RestController
@RequestMapping("/testDatasource")
public class TestController {
@Autowired
private UserService userService;
@Autowired
private DynamicDataSourceUtil dataSourceUtil;
@GetMapping(value = "/getData/{dsType}")
public HttpResonse getData(@PathVariable("dsType") String dsType) {
//http://localhost:8888/testDatasource/getData/master
if (dataSourceUtil.checkDataSourceType(dsType)){
DynamicDataSourceContextHolder.setDataSourceType(dsType);
}
List<User> list = userService.list();
return new HttpResonse().successful(list);
}
}
虽然我们经过上面的方法已经可以做到动态切换数据源了,但是它的使用场景非常局限,而且比较麻烦。下面我们就使用aop的形式来实现动态切换。
package com.example.dynamicdatasourcetest.annotation;
import com.example.dynamicdatasourcetest.datasource.DataSourceConst;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface DataSource {
String value() default DataSourceConst.DEFAULT_DATASOURCE;
}
package com.example.dynamicdatasourcetest.aop;
import com.example.dynamicdatasourcetest.annotation.DataSource;
import com.example.dynamicdatasourcetest.datasource.DynamicDataSourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class DataSourceAspect {
@Pointcut("@annotation(com.example.dynamicdatasourcetest.annotation.DataSource) || @within(com.example.dynamicdatasourcetest.annotation.DataSource)")
public void pc(){
}
@Around("pc()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
DataSource dataSource = getDataSource(proceedingJoinPoint);
if (dataSource !=null){
String value = dataSource.value();
DynamicDataSourceContextHolder.setDataSourceType(value);
}
try {
return proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
return null;
}
private DataSource getDataSource(ProceedingJoinPoint proceedingJoinPoint) {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
//方法上找
DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (annotation != null) {
return annotation;
}
//类上找
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}
这样是不是很方便,直接在方法上打注解就可以了,要注意的是,这两种方法不能同时使用,因为他最终还是要使用service上的注解。其次就是方法优先级大于类上的加的优先级。
package com.example.dynamicdatasourcetest.service.impl;
import com.example.dynamicdatasourcetest.annotation.DataSource;
import com.example.dynamicdatasourcetest.bean.User;
import com.example.dynamicdatasourcetest.datasource.DataSourceConst;
import com.example.dynamicdatasourcetest.mapper.UserMapper;
import com.example.dynamicdatasourcetest.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
@DataSource(DataSourceConst.DATASOURCE_TEST2)
public List<User> list() {
return userMapper.selectAll();
}
}
由于其它代码都比较简单,而且配置文件这些都有差异,所以在上面只贴了主要代码整个项目我会上传到gitee上。
代码非常简单,有兴趣可以pull下来看看。
gitee地址
此项目是一个练习项目,可以通过他学习自定义加载数据源,切换数据源,ThreadLocal使用,注解,aop等等。
git上也有很多现成的解决方案,比如baomido(mybatis-plus)开源的组件,完全可以作为项目解决方案,兼容性很好。==> dynamic-datasource-spring-boot-starter
说起spring bean的生命周期,那就不得不放上一张图了。
其实简单来说就是,bean的生命周期就只有4步,实例化 → 属性赋值 → 初始化 → 销毁。我们一般使用bean就是在初始化后面到销毁前。如果想要对bean做一些特殊的操作,或者在使用之前做一些初始化行为,那就必须要使用到spring提供的接口。
首先说说这个InstantiationAwareBeanPostProcessor接口,根据名字可以看出他是和实例化有关,这个类里面有四个方法,其中postProcessBeforeInstantiation,postProcessAfterInstantiation看名字 就知道是在实例化之前后做一些事情。那么这两个方法到底是做什么的呢。其他两个不是很清楚,有兴趣自己查阅。
default Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
return null;//返回一个对象(废话)-->怎么用?一般是在bean实例化前去自定义构造一个bean然后替换当前的bean
}
default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
return true;//true 表示在初始化结束后,不调用后面的生命周期方法例如 postProcessProperties等等
}
@Nullable
default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
return null;
}
/** @deprecated */
@Deprecated
@Nullable
default PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
return pvs;
}
说完InstantiationAwareBeanPostProcessor,该说说BeanPostProcessor 接口了,其实InstantiationAwareBeanPostProcessor本身就继承了BeanPostProcessor 接口。意味着你可以通过重写,控制实例化和初始化过程。
下面是BeanPostProcessor ,根据方法名称就知道,即使在初始化前后做一些事情
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
说到这有人就说,那这也没有afterPropertiesSet方法啊,这是InitializingBean接口中定义的抽象方法,这个方法的实现会在属性赋值之后调用,说直白点就是,在@Autowired等各种注入完成之后执行。这是一个什么状态点呢?属于说是spring已经干差不多了,现在想做什么最后的增强,赶紧告诉spring。aop就是在这一步完成的。
常用的就讲完了,剩下的就靠自己慢慢研究了。
这里还是先给出一个比较专业的概念定义:
Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的
Advice。
Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
Target(目标对象):织入 Advice 的目标对象.。 Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程.
aop实现就不多说了,面试题都有,我们就讲实战怎么去用。
工作中一般都是基于AspectJ实现AOP操作,AspectJ不是Spring组成部分,独立AOP框架,一般把AspectJ和Spring框架一起使用,进行AOP操作。
DataSourceAspect就是一个简单的使用样例,一般步骤如下:
3.Advice,在切入点上执行的增强处理,主要有五个注解:
@Before 在切点方法之前执行
@After 在切点方法之后执行
@AfterReturning 切点方法返回后执行
@AfterThrowing 切点方法抛异常执行
@Around 属于环绕增强,能控制切点执行前,执行后