[Kotlin]SpringBoot根据域名动态切换数据源,配置JPA以及Hibernate的事务

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))
    }
}

 

你可能感兴趣的:(动态数据源,多数据源,SpringBoot,Hibernate,JPA,JAVA,Kotlin)