1、运行环境
Kotlin、SpringBoot(1.5.13)、Hibernate5、Druid
备注:需要JAVA的同学可以根据插件或者自己转成JAVA,这里提供一个配置的思路
2、配置文件(application.yml)
spring:
application:
name: clangs
datasource:
hj:
password: password
name: hjDataSource
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: @spring.hj.datasource.url@&useSSL=true&verifyServerCertificate=false
username: username
gym:
password: password
name: gymDataSource
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: @spring.gym.datasource.url@&useSSL=true&verifyServerCertificate=false
username: username
druid:
min-idle: 10
test-while-idle: true
max-active: 30
validation-query: select 1
initial-size: 10
connection-init-sqls: set names utf8mb4
default-auto-commit: false
jpa:
show-sql: true
open-in-view: false
hibernate:
use-new-id-generator-mappings: true
database-platform: MySQL5InnoDB
properties:
hibernate:
current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext
备注:根据需要酌情修改
3、切换数据源思路
分别注册配置文件中提到的数据源,之后注册routingDataSource(DynamicDataSource),把上述数据源写入routingDataSource(DynamicDataSource)。切换数据源的时机是Controller执行时,用AOP做这件事,在切面里可以通过RequestContextHolder获取Request,有了Request就可以获取域名了,根据域名设定ThreadLocal的值,DynamicDataSource通过利用ThreadLocal来切换。
4、代码
/**
* Jpa、Hibernate5等的上下文配置
*/
@Configuration
@Import(DataSourceConfig::class, JpaEntityManager::class)
open class ContextConfig {
@Bean
open fun sessionFactory(@Qualifier("entityManagerFactory") emf: EntityManagerFactory): SessionFactory {
return emf.unwrap(SessionFactory::class.java)
}
@Bean("transactionManager")
open fun transactionManager(@Qualifier("sessionFactory") sessionFactory: SessionFactory) = HibernateTransactionManager().apply { this.sessionFactory = sessionFactory }
}
这个代码注册了Hibernate的SessionFactory以及SessionFactory的事务管理器,如果代码启动后SessionFactory无法使用再注册下面这个过滤器
@Bean
open fun openSession(): FilterRegistrationBean {
val bean = FilterRegistrationBean()
bean.initParameters = mapOf("sessionFactoryBeanName" to "sessionFactory")
bean.filter = OpenSessionInViewFilter() as Filter?
bean.addUrlPatterns("/*")
return bean
}
接下来是DataSource的配置
@Configuration
@Import(DataSourceAdvice::class)
open class DataSourceConfig {
@Primary
@Bean(name = ["hjDataSource"])
@ConfigurationProperties(prefix = "spring.datasource.hj")
open fun hjDataSource(): DataSource {
return DataSourceBuilder.create().build()
}
@Bean(name = ["gymDataSource"])
@ConfigurationProperties(prefix = "spring.datasource.gym")
open fun gymDataSource(): DataSource {
return DataSourceBuilder.create().build()
}
@Bean(name = ["routingDataSource"])
open fun routingDataSource(@Qualifier("hjDataSource") hyDataSource: DataSource,@Qualifier("gymDataSource") gymDataSource: DataSource): AbstractRoutingDataSource {
return DynamicDataSource().apply {
this.setDefaultTargetDataSource(hjDataSource())
this.setTargetDataSources(mapOf("hyDataSource" to hjDataSource(),"gymDataSource" to gymDataSource()))
}
}
}
这里注册了配置文件中提到的数据源配置,以及数据源路由。DataSourceAdvice是Controller的切面,在Controller执行前切换数据源,以及事务的管理。
DynamicDataSource是动态的数据源,我们在这里调用切换代码
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceSwitcher.getDataSource();
}
}
DataSourceSwitcher类是做切换工作的,切换就是通过DataSourceSwitcher内的ThreadLocal实现
class DataSourceSwitcher {
private static final ThreadLocal holder = new InheritableThreadLocal();
static void setDataSource(String type) {
holder.set(type);
}
static String getDataSource() {
String lookUpKey = holder.get();
return lookUpKey == null ? "hjDataSource" : lookUpKey;
}
static void clear() {
holder.remove();
}
}
getDataSource里要设置一个默认的数据源,当获取的数据源是空的时候使用。然后我们用DataSourceAdvice类切换。
DataSourceAdvice类用JAVA写的(因为IDEA里的Kotlin版没有提示切面=_=)
/**
* DataSource的切面
* 用于切换DataSource、开启事务
*/
@Aspect
@Configuration
@EnableTransactionManagement
public class DataSourceAdvice {
private final static Logger logger = Logger.getLogger(DataSourceAdvice.class);
@Resource
private HibernateTransactionManager transactionManager;
@Pointcut("execution(* cc.*.web..*.*(..))")
private void aspect() {
/**
* 切换数据源的切点
*/
}
@Before("aspect()")
public void Before(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
logger.info("请求开始, 各个参数, url: " + request.getRequestURL() + ", method: " + request.getMethod() + ", uri: " + request.getRequestURI() + ", queryString: " + request.getQueryString());
DataSourceSwitcher.setDataSource(Objects.requireNonNull(Context.INSTANCE.getCurrentSite()).getNumber() + "DataSource");
}
@After("aspect()")
public void After(JoinPoint joinPoint) {
logger.info("清除 datasource router...");
DataSourceSwitcher.clear();
}
@Bean("txAdvice")
public TransactionInterceptor txAdvice() {
DefaultTransactionAttribute txAttrRequired = new DefaultTransactionAttribute();
txAttrRequired.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
DefaultTransactionAttribute txAttrRequiredReadonly = new DefaultTransactionAttribute();
txAttrRequiredReadonly.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
txAttrRequiredReadonly.setReadOnly(true);
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
source.addTransactionalMethod("add*", txAttrRequired);
source.addTransactionalMethod("save*", txAttrRequired);
source.addTransactionalMethod("delete*", txAttrRequired);
source.addTransactionalMethod("remove*", txAttrRequired);
source.addTransactionalMethod("modify*", txAttrRequired);
source.addTransactionalMethod("update*", txAttrRequired);
source.addTransactionalMethod("exec*", txAttrRequired);
source.addTransactionalMethod("tx*", txAttrRequired);
source.addTransactionalMethod("set*", txAttrRequired);
source.addTransactionalMethod("get*", txAttrRequiredReadonly);
source.addTransactionalMethod("query*", txAttrRequiredReadonly);
source.addTransactionalMethod("find*", txAttrRequiredReadonly);
source.addTransactionalMethod("list*", txAttrRequiredReadonly);
source.addTransactionalMethod("count*", txAttrRequiredReadonly);
source.addTransactionalMethod("is*", txAttrRequiredReadonly);
return new TransactionInterceptor(transactionManager, source);
}
@Bean
public Advisor txAdviceAdvisor(@Qualifier("txAdvice") TransactionInterceptor txAdvice) {
AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
aspectJExpressionPointcut.setExpression("execution(* cc.*.service..*.*(..))");
return new DefaultPointcutAdvisor(aspectJExpressionPointcut, txAdvice);
}
}
具体切换的代码在Before方法中,这里有一句
DataSourceSwitcher.setDataSource(Objects.requireNonNull(Context.INSTANCE.getCurrentSite()).getNumber() + "DataSource");
这里可以换成根据Request获取域名,根据域名自行给出数据源名,因为我的项目有直接获取当前请求所属站点的功能,所以拿来用。Context和DataSourceSwitcher一样通过ThreadLocal实现获取当前站点的信息,Context放到了Filter里执行,在Filter里查询数据库结果查询数据库的操作(Filter)优先AOP执行,导致数据源切换失败,holder.get()一直是空。如果有同学和我有一样的需求,可以在启动Spring是注册一个Site的Bean,Filter了设定Context时使用这个Bean查Site信息而不会去查数据库,就不会有切面执行前就有查询数据库的操作了,此时holder.get()不会为空。当然通过
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
这行代码直接获取域名而不去设定Context就没这个问题了。(话说为什么不把Context放进AOP里【痴呆.jpg】)
另外注意事务和切换数据源的切点不要切同一层。
下面是JPA的配置,根据需要酌情修改包名
@Configuration
@EnableConfigurationProperties(JpaProperties::class)
@EnableJpaRepositories(value = ["cc.messcat.dao.*", "cc.messcat.common.dao"],entityManagerFactoryRef = "entityManagerFactory",transactionManagerRef = "transactionManager")
open class JpaEntityManager {
@Autowired
private val jpaProperties: JpaProperties? = null
@Resource(name = "routingDataSource")
private val routingDataSource: AbstractRoutingDataSource? = null
@Bean(name = ["entityManagerFactoryBean"])
open fun entityManagerFactoryBean(builder: EntityManagerFactoryBuilder): LocalContainerEntityManagerFactoryBean {
return builder
.dataSource(routingDataSource)//关键:注入routingDataSource
.properties(jpaProperties!!.properties.apply {
//要设置这个属性,实现 CamelCase -> UnderScore 的转换
this["hibernate.physical_naming_strategy"] = "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy" }
)
.packages("cc.messcat.entity", "cc.messcat.common.entity")
.persistenceUnit("myPersistenceUnit")
.build()
}
@Primary
@Bean(name = ["entityManagerFactory"])
open fun entityManagerFactory(builder: EntityManagerFactoryBuilder): EntityManagerFactory {
return this.entityManagerFactoryBean(builder).getObject()
}
@Primary
@Bean(name = ["transactionManager"])
open fun transactionManager(builder: EntityManagerFactoryBuilder): PlatformTransactionManager {
return JpaTransactionManager(entityManagerFactory(builder))
}
}