diff --git a/README.md b/README.md index fc1f137b..f98c11cc 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ 4. 统一的认证入口,方便的安全认证扩展,可实现多种方式的认证,且支持表单与接口 5. 灵活的权限钩子,既可全局进行权限验证、亦可定义于类与方法,验证方式易与扩展 6. 细粒度的RBAC权限控制,可自定义验证方式,支持数据范围注入 +7. 动态数据源+多数据源事务管理 @@ -29,7 +30,7 @@ - `citrus-boot-starter` 项目自动配置相关 - `citrus-main` 项目的运行入口(体验开箱即用的快感) - `citrus-security` 项目安全相关的代码,统一认证、验证码类型、鉴权、jwt等 [安全模块传送门](https://github.com/Yiuman/citrus/tree/master/doc/安全模块设计.md) -- `citrus-support` 项目通用支持相关的代码,通用Service层、Controller层,工具类、缓存、异常、注入、数据结构及相关扩展 [通用CRUD指南](https://github.com/Yiuman/citrus/tree/master/doc/通用CRUD指南.md) +- `citrus-support` 项目通用支持相关的代码,通用Service层、Controller层,工具类、缓存、异常、注入、数据结构、动态数据源及相关扩展 [通用CRUD指南](https://github.com/Yiuman/citrus/tree/master/doc/通用CRUD指南.md) - `citrus-system` 项目系统设计的主要实现 包含用户、角色、权限、资源、菜单、数据范围等模块的实现与处理,数据范围注入也在这里 [权限数据范围设计](https://github.com/Yiuman/citrus/tree/master/doc/权限设计.md) @@ -44,7 +45,7 @@ com.github.yiuman citrus-boot-starter - 0.0.7 + 0.0.8 ``` diff --git a/citrus-boot-starter/pom.xml b/citrus-boot-starter/pom.xml index 9811e76f..86f40b07 100644 --- a/citrus-boot-starter/pom.xml +++ b/citrus-boot-starter/pom.xml @@ -5,7 +5,7 @@ citrus com.github.yiuman - 0.0.7 + 0.0.8 4.0.0 @@ -16,21 +16,21 @@ com.github.yiuman citrus-support - ${citrus.security.version} + ${citrus.version} compile com.github.yiuman citrus-security - ${citrus.security.version} + ${citrus.version} compile com.github.yiuman citrus-system - ${citrus.security.version} + ${citrus.version} compile @@ -42,6 +42,10 @@ org.springframework.boot spring-boot-autoconfigure + + org.springframework.boot + spring-boot-starter-jta-atomikos + org.springframework.boot diff --git a/citrus-boot-starter/src/main/java/com/github/yiuman/citrus/starter/CitrusAutoConfiguration.java b/citrus-boot-starter/src/main/java/com/github/yiuman/citrus/starter/CitrusAutoConfiguration.java index 330e312c..6645f2b2 100644 --- a/citrus-boot-starter/src/main/java/com/github/yiuman/citrus/starter/CitrusAutoConfiguration.java +++ b/citrus-boot-starter/src/main/java/com/github/yiuman/citrus/starter/CitrusAutoConfiguration.java @@ -15,6 +15,7 @@ import com.github.yiuman.citrus.security.jwt.JwtSecurityConfigurerAdapter; import com.github.yiuman.citrus.security.properties.CitrusProperties; import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @@ -47,7 +48,7 @@ @ComponentScan("com.github.yiuman.citrus.system") }) @MapperScan(basePackages = "com.github.yiuman.citrus.system.mapper") -@Import({SystemDefaultBeanConfiguration.class, VerifyConfiguration.class}) +@Import({SystemDefaultBeanConfiguration.class, VerifyConfiguration.class,DynamicDataSourceAutoConfiguration.class}) @EnableWebSecurity public class CitrusAutoConfiguration { diff --git a/citrus-boot-starter/src/main/java/com/github/yiuman/citrus/starter/DynamicDataSourceAutoConfiguration.java b/citrus-boot-starter/src/main/java/com/github/yiuman/citrus/starter/DynamicDataSourceAutoConfiguration.java new file mode 100644 index 00000000..22a8e52f --- /dev/null +++ b/citrus-boot-starter/src/main/java/com/github/yiuman/citrus/starter/DynamicDataSourceAutoConfiguration.java @@ -0,0 +1,303 @@ +package com.github.yiuman.citrus.starter; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.pool.xa.DruidXADataSource; +import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer; +import com.baomidou.mybatisplus.autoconfigure.SpringBootVFS; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; +import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator; +import com.baomidou.mybatisplus.core.injector.ISqlInjector; +import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.github.yiuman.citrus.support.datasource.*; +import org.apache.ibatis.mapping.DatabaseIdProvider; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.scripting.LanguageDriver; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.type.TypeHandler; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import javax.sql.DataSource; +import java.util.*; +import java.util.function.Consumer; + +/** + * 动态数据源自动配置 + * + * @author yiuman + * @date 2020/12/1 + */ +@SuppressWarnings("rawtypes") +@Configuration +@EnableConfigurationProperties({ DynamicDataSourceProperties.class,DataSourceProperties.class, MybatisPlusProperties.class}) +@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class}) +public class DynamicDataSourceAutoConfiguration implements InitializingBean { + + private final DataSourceProperties dataSourceProperties; + + private final DynamicDataSourceProperties dynamicDataSourceProperties; + + private final MybatisPlusProperties mybatisPlusProperties; + + private final org.apache.ibatis.plugin.Interceptor[] interceptors; + + private final TypeHandler[] typeHandlers; + + private final LanguageDriver[] languageDrivers; + + private final ResourceLoader resourceLoader; + + private final DatabaseIdProvider databaseIdProvider; + + private final List configurationCustomizers; + + private final List mybatisPlusPropertiesCustomizers; + + private final ApplicationContext applicationContext; + + public DynamicDataSourceAutoConfiguration(DataSourceProperties dataSourceProperties, + DynamicDataSourceProperties dynamicDataSourceProperties, + MybatisPlusProperties mybatisPlusProperties, + ObjectProvider interceptorsProvider, + ObjectProvider typeHandlersProvider, + ObjectProvider languageDriversProvider, + ResourceLoader resourceLoader, + ObjectProvider databaseIdProvider, + ObjectProvider> configurationCustomizersProvider, + ObjectProvider> mybatisPlusPropertiesCustomizerProvider, + ApplicationContext applicationContext) { + this.dataSourceProperties = dataSourceProperties; + this.dynamicDataSourceProperties = dynamicDataSourceProperties; + this.mybatisPlusProperties = mybatisPlusProperties; + this.interceptors = interceptorsProvider.getIfAvailable(); + this.typeHandlers = typeHandlersProvider.getIfAvailable(); + this.languageDrivers = languageDriversProvider.getIfAvailable(); + this.resourceLoader = resourceLoader; + this.databaseIdProvider = databaseIdProvider.getIfAvailable(); + this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable(); + this.mybatisPlusPropertiesCustomizers = mybatisPlusPropertiesCustomizerProvider.getIfAvailable(); + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() { + if (!CollectionUtils.isEmpty(mybatisPlusPropertiesCustomizers)) { + mybatisPlusPropertiesCustomizers.forEach(i -> i.customize(mybatisPlusProperties)); + } + checkConfigFileExists(); + } + + private void checkConfigFileExists() { + if (this.mybatisPlusProperties.isCheckConfigLocation() && StringUtils.hasText(this.mybatisPlusProperties.getConfigLocation())) { + Resource resource = this.resourceLoader.getResource(this.mybatisPlusProperties.getConfigLocation()); + Assert.state(resource.exists(), + "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)"); + } + } + + /** + * 配置动态数据源 + * + * @return DataSource + */ + @Bean + @ConditionalOnMissingBean + public DataSource dynamicDatasource() { + DynamicDataSource dynamicDataSource = new DynamicDataSource(); + Map dataSourcePropertiesMap = dynamicDataSourceProperties.getDatasource(); + int dataSourceSize = Objects.nonNull(dataSourcePropertiesMap) ? dataSourcePropertiesMap.size() : 0; + Map dataSourceMap = new HashMap<>(dataSourceSize + 1); + DataSource defaultDataSource; + String primary = Optional.of(dynamicDataSourceProperties.getPrimary()).orElse(""); + if (dataSourceSize > 0) { + defaultDataSource = buildDruidXADataSource(primary, dataSourceProperties); + dataSourcePropertiesMap.forEach((key, properties) -> dataSourceMap.put(key, buildDruidXADataSource(key, properties))); + } else { + defaultDataSource = buildDruidDataSource(dataSourceProperties); + } + dataSourceMap.put(primary, defaultDataSource); + dynamicDataSource.setTargetDataSources(dataSourceMap); + dynamicDataSource.setDefaultTargetDataSource(defaultDataSource); + return dynamicDataSource; + } + + @Bean + public DynamicSqlSessionTemplate sqlSessionTemplate() throws Exception { + Map dataSourcePropertiesMap = dynamicDataSourceProperties.getDatasource(); + int dataSourceSize = Objects.nonNull(dataSourcePropertiesMap) ? dataSourcePropertiesMap.size() : 0; + Map sqlSessionFactoryMap = new HashMap<>(dataSourceSize + 1); + DataSource defaultDataSource; + String primary = Optional.of(dynamicDataSourceProperties.getPrimary()).orElse(""); + if (dataSourceSize > 0) { + defaultDataSource = buildDruidXADataSource(primary, dataSourceProperties); + for (Map.Entry entry : dataSourcePropertiesMap.entrySet()) { + sqlSessionFactoryMap.put(entry.getKey(), createSqlSessionFactory(buildDruidXADataSource(entry.getKey(), entry.getValue()))); + } + } else { + defaultDataSource = buildDruidDataSource(dataSourceProperties); + } + SqlSessionFactory defaultSqlSessionFactory = createSqlSessionFactory(defaultDataSource); + sqlSessionFactoryMap.put(primary, defaultSqlSessionFactory); + DynamicSqlSessionTemplate dynamicSqlSessionTemplate = new DynamicSqlSessionTemplate(defaultSqlSessionFactory); + dynamicSqlSessionTemplate.setTargetSqlSessionFactories(sqlSessionFactoryMap); + dynamicSqlSessionTemplate.setDefaultTargetSqlSessionFactory(defaultSqlSessionFactory); + dynamicSqlSessionTemplate.setStrict(dynamicDataSourceProperties.isStrict()); + return dynamicSqlSessionTemplate; + } + + @Bean + public DynamicDataSourceAnnotationAdvisor dynamicDataSourceAnnotationAdvisor() { + return new DynamicDataSourceAnnotationAdvisor(new DynamicDataSourceAnnotationInterceptor()); + } + + + /** + * 根据配置构建的druid数据源 + * + * @param properties 数据源配置 + * @return DruidDataSource + */ + public DataSource buildDruidDataSource(DataSourceProperties properties) { + DruidDataSource druidDataSource = new DruidDataSource(); + druidDataSource.setUrl(properties.getUrl()); + druidDataSource.setUsername(properties.getUsername()); + druidDataSource.setPassword(properties.getPassword()); + druidDataSource.setDriverClassName(properties.getDriverClassName()); + return druidDataSource; + } + + + /** + * 根据配置构建XA数据源 + * + * @param resourceName 资源名,用于定义XA唯一资源 + * @param properties 数据源配置 + * @return XA数据源 + */ + public DataSource buildDruidXADataSource(String resourceName, DataSourceProperties properties) { + DruidXADataSource druidDataSource = new DruidXADataSource(); + druidDataSource.setUrl(properties.getUrl()); + druidDataSource.setUsername(properties.getUsername()); + druidDataSource.setPassword(properties.getPassword()); + druidDataSource.setDriverClassName(properties.getDriverClassName()); + + AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean(); + atomikosDataSourceBean.setXaDataSource(druidDataSource); + atomikosDataSourceBean.setUniqueResourceName(resourceName); + return atomikosDataSourceBean; + } + + private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception { + // TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean + MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean(); + factory.setDataSource(dataSource); + factory.setVfs(SpringBootVFS.class); + if (StringUtils.hasText(this.mybatisPlusProperties.getConfigLocation())) { + factory.setConfigLocation(this.resourceLoader.getResource(this.mybatisPlusProperties.getConfigLocation())); + } + applyConfiguration(factory); + if (this.mybatisPlusProperties.getConfigurationProperties() != null) { + factory.setConfigurationProperties(this.mybatisPlusProperties.getConfigurationProperties()); + } + + if (this.databaseIdProvider != null) { + factory.setDatabaseIdProvider(this.databaseIdProvider); + } + if (StringUtils.hasLength(this.mybatisPlusProperties.getTypeAliasesPackage())) { + factory.setTypeAliasesPackage(this.mybatisPlusProperties.getTypeAliasesPackage()); + } + if (this.mybatisPlusProperties.getTypeAliasesSuperType() != null) { + factory.setTypeAliasesSuperType(this.mybatisPlusProperties.getTypeAliasesSuperType()); + } + if (StringUtils.hasLength(this.mybatisPlusProperties.getTypeHandlersPackage())) { + factory.setTypeHandlersPackage(this.mybatisPlusProperties.getTypeHandlersPackage()); + } + if (!ObjectUtils.isEmpty(this.typeHandlers)) { + factory.setTypeHandlers(this.typeHandlers); + } + if (!ObjectUtils.isEmpty(this.mybatisPlusProperties.resolveMapperLocations())) { + factory.setMapperLocations(this.mybatisPlusProperties.resolveMapperLocations()); + } + + // TODO 对源码做了一定的修改(因为源码适配了老旧的mybatis版本,但我们不需要适配) + Class extends LanguageDriver> defaultLanguageDriver = this.mybatisPlusProperties.getDefaultScriptingLanguageDriver(); + if (!ObjectUtils.isEmpty(this.languageDrivers)) { + factory.setScriptingLanguageDrivers(this.languageDrivers); + } + Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver); + if (!ObjectUtils.isEmpty(this.interceptors)) { + factory.setPlugins(this.interceptors); + } + // TODO 自定义枚举包 + if (StringUtils.hasLength(this.mybatisPlusProperties.getTypeEnumsPackage())) { + factory.setTypeEnumsPackage(this.mybatisPlusProperties.getTypeEnumsPackage()); + } + // TODO 此处必为非 NULL + GlobalConfig globalConfig = GlobalConfigUtils.defaults(); + //去除打印 + globalConfig.setBanner(false); + // TODO 注入填充器 + this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler); + // TODO 注入主键生成器 + this.getBeanThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerator(i)); + // TODO 注入sql注入器 + this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector); + // TODO 注入ID生成器 + this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator); +// // TODO 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean + factory.setGlobalConfig(globalConfig); + return factory.getObject(); + } + + // TODO 入参使用 MybatisSqlSessionFactoryBean + private void applyConfiguration(MybatisSqlSessionFactoryBean factory) { + // TODO 使用 MybatisConfiguration + MybatisConfiguration configuration = this.mybatisPlusProperties.getConfiguration(); + if (configuration == null && !StringUtils.hasText(this.mybatisPlusProperties.getConfigLocation())) { + configuration = new MybatisConfiguration(); + } + if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) { + for (ConfigurationCustomizer customizer : this.configurationCustomizers) { + customizer.customize(configuration); + } + } + + factory.setConfiguration(configuration); + } + + /** + * 检查spring容器里是否有对应的bean,有则进行消费 + * + * @param clazz class + * @param consumer 消费 + * @param 泛型 + */ + private void getBeanThen(Class clazz, Consumer consumer) { + if (this.applicationContext.getBeanNamesForType(clazz, false, false).length > 0) { + consumer.accept(this.applicationContext.getBean(clazz)); + } + } + + +} diff --git a/citrus-main/pom.xml b/citrus-main/pom.xml index 35223752..b52dcd5f 100644 --- a/citrus-main/pom.xml +++ b/citrus-main/pom.xml @@ -29,7 +29,7 @@ com.github.yiuman citrus-boot-starter - 0.0.7 + 0.0.8 diff --git a/citrus-main/src/main/java/com/github/yiuman/citrus/CitrusApplication.java b/citrus-main/src/main/java/com/github/yiuman/citrus/CitrusApplication.java index f6f95d90..7da8b6e7 100644 --- a/citrus-main/src/main/java/com/github/yiuman/citrus/CitrusApplication.java +++ b/citrus-main/src/main/java/com/github/yiuman/citrus/CitrusApplication.java @@ -13,4 +13,5 @@ public class CitrusApplication { public static void main(String[] args) { SpringApplication.run(CitrusApplication.class, args); } + } diff --git a/citrus-main/src/main/java/com/github/yiuman/citrus/datasource/DictionaryMapperOne.java b/citrus-main/src/main/java/com/github/yiuman/citrus/datasource/DictionaryMapperOne.java new file mode 100644 index 00000000..769970db --- /dev/null +++ b/citrus-main/src/main/java/com/github/yiuman/citrus/datasource/DictionaryMapperOne.java @@ -0,0 +1,17 @@ +package com.github.yiuman.citrus.datasource; + +import com.github.yiuman.citrus.support.crud.mapper.CrudMapper; +import com.github.yiuman.citrus.support.datasource.DataSource; +import com.github.yiuman.citrus.system.entity.Dictionary; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Repository; + +/** + * @author yiuman + * @date 2020/12/2 + */ +@Repository +@Mapper +@DataSource +public interface DictionaryMapperOne extends CrudMapper { +} \ No newline at end of file diff --git a/citrus-main/src/main/java/com/github/yiuman/citrus/datasource/DictionaryMapperTwo.java b/citrus-main/src/main/java/com/github/yiuman/citrus/datasource/DictionaryMapperTwo.java new file mode 100644 index 00000000..706c6a54 --- /dev/null +++ b/citrus-main/src/main/java/com/github/yiuman/citrus/datasource/DictionaryMapperTwo.java @@ -0,0 +1,18 @@ +package com.github.yiuman.citrus.datasource; + +import com.github.yiuman.citrus.support.crud.mapper.CrudMapper; +import com.github.yiuman.citrus.support.datasource.DataSource; +import com.github.yiuman.citrus.system.entity.Dictionary; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Repository; + +/** + * @author yiuman + * @date 2020/12/2 + */ +@Repository +@Mapper +@DataSource("txServer") +public interface DictionaryMapperTwo extends CrudMapper { + +} diff --git a/citrus-main/src/main/java/com/github/yiuman/citrus/datasource/DynamicController.java b/citrus-main/src/main/java/com/github/yiuman/citrus/datasource/DynamicController.java new file mode 100644 index 00000000..944401f0 --- /dev/null +++ b/citrus-main/src/main/java/com/github/yiuman/citrus/datasource/DynamicController.java @@ -0,0 +1,41 @@ +package com.github.yiuman.citrus.datasource; + +import com.github.yiuman.citrus.system.entity.Dictionary; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author yiuman + * @date 2020/12/2 + */ +@RestController +public class DynamicController { + + private final DictionaryMapperOne dictionaryMapperOne; + + private final DictionaryMapperTwo dictionaryMapperTwo; + + public DynamicController(DictionaryMapperOne dictionaryMapperOne, DictionaryMapperTwo dictionaryMapperTwo) { + this.dictionaryMapperOne = dictionaryMapperOne; + this.dictionaryMapperTwo = dictionaryMapperTwo; + } + + @PostMapping("/test/transactional") + @Transactional(rollbackFor = Throwable.class) + public void testTransactional() { + Dictionary dictionary2 = new Dictionary(); + dictionary2.setDictCode("345345"); + dictionary2.setDictName("测试2"); +// + dictionaryMapperTwo.saveEntity(dictionary2); + +// int error = 1/0; + + Dictionary dictionary = new Dictionary(); + dictionary.setDictCode("123123"); + dictionary.setDictName("测试1"); + dictionaryMapperOne.saveEntity(dictionary); + + } +} diff --git a/citrus-main/src/main/resources/application.yml b/citrus-main/src/main/resources/application.yml index 09e5b41f..5d64c5cd 100644 --- a/citrus-main/src/main/resources/application.yml +++ b/citrus-main/src/main/resources/application.yml @@ -4,5 +4,11 @@ spring: url: jdbc:mysql://localhost:3306/citrus?zeroDateTimeBehavior=convertToNull&characterEncoding=UTF-8 username: root password: yiuman + multiples: + txServer: + driver-class-name: com.mysql.jdbc.Driver + url: jdbc:mysql://42.192.95.146:3306/citrus?zeroDateTimeBehavior=convertToNull&characterEncoding=UTF-8 + username: root + password: yiuman server: port: 8082 \ No newline at end of file diff --git a/citrus-security/pom.xml b/citrus-security/pom.xml index 1db9db62..effcbc7b 100644 --- a/citrus-security/pom.xml +++ b/citrus-security/pom.xml @@ -5,7 +5,7 @@ com.github.yiuman citrus - 0.0.7 + 0.0.8 4.0.0 @@ -17,7 +17,7 @@ com.github.yiuman citrus-support - ${citrus.security.version} + ${citrus.version} org.springframework.boot diff --git a/citrus-support/pom.xml b/citrus-support/pom.xml index 4eb3ea40..2fd50a99 100644 --- a/citrus-support/pom.xml +++ b/citrus-support/pom.xml @@ -5,7 +5,7 @@ citrus com.github.yiuman - 0.0.7 + 0.0.8 4.0.0 @@ -28,6 +28,11 @@ org.springframework.boot spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-aop + org.springframework.boot spring-boot-starter-validation @@ -36,6 +41,11 @@ org.springframework.boot spring-boot-starter-data-redis + + com.alibaba + druid-spring-boot-starter + ${druid.version} + org.javassist javassist diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/crud/mapper/CrudMapper.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/crud/mapper/CrudMapper.java index 7f5171bb..8da20e80 100644 --- a/citrus-support/src/main/java/com/github/yiuman/citrus/support/crud/mapper/CrudMapper.java +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/crud/mapper/CrudMapper.java @@ -39,7 +39,7 @@ default boolean saveEntity(T entity) { if (StringUtils.isBlank(keyProperty)) { return SqlHelper.retBool(insert(entity)); } - Object idVal = ReflectionKit.getMethodValue(cls, entity, tableInfo.getKeyProperty()); + Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty()); return StringUtils.checkValNull(idVal) || Objects.isNull(selectById((Serializable) idVal)) ? SqlHelper.retBool(insert(entity)) : SqlHelper.retBool(updateById(entity)); diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/crud/service/KeyBasedService.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/crud/service/KeyBasedService.java index a63ca77c..5a2f0b30 100644 --- a/citrus-support/src/main/java/com/github/yiuman/citrus/support/crud/service/KeyBasedService.java +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/crud/service/KeyBasedService.java @@ -36,7 +36,7 @@ default Class getKeyType() { @SuppressWarnings("unchecked") default K getKey(E entity) { TableInfo tableInfo = TableInfoHelper.getTableInfo(getEntityType()); - return Objects.nonNull(tableInfo) ? (K) ReflectionKit.getMethodValue(getEntityType(), entity, tableInfo.getKeyProperty()) : null; + return Objects.nonNull(tableInfo) ? (K) ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty()) : null; } /** diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DataSource.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DataSource.java new file mode 100644 index 00000000..de6adcff --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DataSource.java @@ -0,0 +1,19 @@ +package com.github.yiuman.citrus.support.datasource; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 动态数据源注解 + * + * @author yiuman + * @date 2020/12/1 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface DataSource { + + String value() default ""; +} \ No newline at end of file diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DataSourceClassResolver.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DataSourceClassResolver.java new file mode 100644 index 00000000..20563636 --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DataSourceClassResolver.java @@ -0,0 +1,209 @@ +package com.github.yiuman.citrus.support.datasource; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.MethodClassKey; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.util.ClassUtils; + +import java.lang.reflect.*; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * DataSource数据源解析器 + * + * @author yiuman + * @date 2020/11/30 + */ +@Slf4j +public class DataSourceClassResolver { + + private static boolean mpEnabled = false; + + private static Field mapperInterfaceField; + + static { + Class> proxyClass = null; + try { + proxyClass = Class.forName("com.baomidou.mybatisplus.core.override.MybatisMapperProxy"); + } catch (ClassNotFoundException e1) { + try { + proxyClass = Class.forName("com.baomidou.mybatisplus.core.override.PageMapperProxy"); + } catch (ClassNotFoundException e2) { + try { + proxyClass = Class.forName("org.apache.ibatis.binding.MapperProxy"); + } catch (ClassNotFoundException ignored) { + } + } + } + if (proxyClass != null) { + try { + mapperInterfaceField = proxyClass.getDeclaredField("mapperInterface"); + mapperInterfaceField.setAccessible(true); + mpEnabled = true; + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } + } + } + + /** + * 缓存方法对应的数据源 + */ + private final Map dsCache = new ConcurrentHashMap<>(); + private final boolean allowedPublicOnly; + + /** + * 加入扩展, 给外部一个修改aop条件的机会 + * + * @param allowedPublicOnly 只允许公共的方法, 默认为true + */ + public DataSourceClassResolver(boolean allowedPublicOnly) { + this.allowedPublicOnly = allowedPublicOnly; + } + + /** + * 从缓存获取数据 + * + * @param method 方法 + * @param targetObject 目标对象 + * @return ds + */ + public String findDSKey(Method method, Object targetObject) { + if (method.getDeclaringClass() == Object.class) { + return ""; + } + Object cacheKey = new MethodClassKey(method, targetObject.getClass()); + String ds = this.dsCache.get(cacheKey); + if (ds == null) { + ds = computeDatasource(method, targetObject); + if (ds == null) { + ds = ""; + } + this.dsCache.put(cacheKey, ds); + } + return ds; + } + + /** + * 查找注解的顺序 + * 1. 当前方法 + * 2. 桥接方法 + * 3. 当前类开始一直找到Object + * 4. 支持mybatis-plus, mybatis-spring + * + * @param method 方法 + * @param targetObject 目标对象 + * @return ds + */ + private String computeDatasource(Method method, Object targetObject) { + if (allowedPublicOnly && !Modifier.isPublic(method.getModifiers())) { + return null; + } + Class> targetClass = targetObject.getClass(); + Class> userClass = ClassUtils.getUserClass(targetClass); + // JDK代理时, 获取实现类的方法声明. method: 接口的方法, specificMethod: 实现类方法 + Method specificMethod = ClassUtils.getMostSpecificMethod(method, userClass); + + specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + // 从当前方法查找 + String dsAttr = findDataSourceAttribute(specificMethod); + if (dsAttr != null) { + return dsAttr; + } + // 从当前方法声明的类查找 + dsAttr = findDataSourceAttribute(specificMethod.getDeclaringClass()); + if (dsAttr != null && ClassUtils.isUserLevelMethod(method)) { + return dsAttr; + } + // 如果存在桥接方法 + if (specificMethod != method) { + // 从桥接方法查找 + dsAttr = findDataSourceAttribute(method); + if (dsAttr != null) { + return dsAttr; + } + // 从桥接方法声明的类查找 + dsAttr = findDataSourceAttribute(method.getDeclaringClass()); + if (dsAttr != null && ClassUtils.isUserLevelMethod(method)) { + return dsAttr; + } + } + return getDefaultDataSourceAttr(targetObject); + } + + /** + * 默认的获取数据源名称方式 + * + * @param targetObject 目标对象 + * @return ds + */ + private String getDefaultDataSourceAttr(Object targetObject) { + Class> targetClass = targetObject.getClass(); + // 如果不是代理类, 从当前类开始, 不断的找父类的声明 + if (!Proxy.isProxyClass(targetClass)) { + Class> currentClass = targetClass; + while (currentClass != Object.class) { + String datasourceAttr = findDataSourceAttribute(currentClass); + if (datasourceAttr != null) { + return datasourceAttr; + } + currentClass = currentClass.getSuperclass(); + } + } + // mybatis-plus, mybatis-spring 的获取方式 + if (mpEnabled) { + final Class> clazz = getMapperInterfaceClass(targetObject); + if (clazz != null) { + String datasourceAttr = findDataSourceAttribute(clazz); + if (datasourceAttr != null) { + return datasourceAttr; + } + // 尝试从其父接口获取 + return findDataSourceAttribute(clazz.getSuperclass()); + } + } + return null; + } + + /** + * 用于处理嵌套代理 + * + * @param target JDK 代理类对象 + * @return InvocationHandler 的 Class + */ + private Class> getMapperInterfaceClass(Object target) { + Object current = target; + while (Proxy.isProxyClass(current.getClass())) { + Object currentRefObject = AopProxyUtils.getSingletonTarget(current); + if (currentRefObject == null) { + break; + } + current = currentRefObject; + } + try { + if (Proxy.isProxyClass(current.getClass())) { + return (Class>) mapperInterfaceField.get(Proxy.getInvocationHandler(current)); + } + } catch (IllegalAccessException ignore) { + } + return null; + } + + /** + * 通过 AnnotatedElement 查找标记的注解, 映射为 DatasourceHolder + * + * @param ae AnnotatedElement + * @return 数据源映射持有者 + */ + private String findDataSourceAttribute(AnnotatedElement ae) { + AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ae, DataSource.class); + if (attributes != null) { + return attributes.getString("value"); + } + return null; + } +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSource.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSource.java new file mode 100644 index 00000000..dc61380e --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSource.java @@ -0,0 +1,23 @@ +package com.github.yiuman.citrus.support.datasource; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +import java.util.Optional; + +/** + * 动态数据源 + * + * @author yiuman + * @date 2020/11/30 + */ +public class DynamicDataSource extends AbstractRoutingDataSource { + + public DynamicDataSource() { + } + + @Override + protected Object determineCurrentLookupKey() { + return Optional.ofNullable(DynamicDataSourceHolder.peek()).orElse(""); + } + +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceAnnotationAdvisor.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceAnnotationAdvisor.java new file mode 100644 index 00000000..4423443b --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceAnnotationAdvisor.java @@ -0,0 +1,132 @@ +/** + * Copyright © 2018 organization baomidou + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.yiuman.citrus.support.datasource; + +import lombok.NonNull; +import org.aopalliance.aop.Advice; +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.Assert; + +import javax.annotation.Nonnull; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * DataSource注解的切面 + * + * @author yiuman + * @date 2020/11/30 + */ +public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { + + private final Advice advice; + + private final Pointcut pointcut; + + public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) { + this.advice = dynamicDataSourceAnnotationInterceptor; + this.pointcut = buildPointcut(); + } + + @Nonnull + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Nonnull + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public void setBeanFactory(@Nonnull BeanFactory beanFactory) throws BeansException { + if (this.advice instanceof BeanFactoryAware) { + ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory); + } + } + + private Pointcut buildPointcut() { + Pointcut cpc = new AnnotationMatchingPointcut(DataSource.class, true); + Pointcut mpc = new AnnotationMethodPoint(DataSource.class); + return new ComposablePointcut(cpc).union(mpc); + } + + /** + * In order to be compatible with the spring lower than 5.0 + */ + private static class AnnotationMethodPoint implements Pointcut { + + private final Class extends Annotation> annotationType; + + public AnnotationMethodPoint(Class extends Annotation> annotationType) { + Assert.notNull(annotationType, "Annotation type must not be null"); + this.annotationType = annotationType; + } + + @Nonnull + @Override + public ClassFilter getClassFilter() { + return ClassFilter.TRUE; + } + + @Nonnull + @Override + public MethodMatcher getMethodMatcher() { + return new AnnotationMethodMatcher(annotationType); + } + + private static class AnnotationMethodMatcher extends StaticMethodMatcher { + private final Class extends Annotation> annotationType; + + public AnnotationMethodMatcher(Class extends Annotation> annotationType) { + this.annotationType = annotationType; + } + + @Override + public boolean matches(@Nonnull Method method, @Nonnull Class> targetClass) { + if (matchesMethod(method)) { + return true; + } + // Proxy classes never have annotations on their redeclared methods. + if (Proxy.isProxyClass(targetClass)) { + return false; + } + // The method may be on an interface, so let's check on the target class as well. + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + return (specificMethod != method && matchesMethod(specificMethod)); + } + + private boolean matchesMethod(Method method) { + return AnnotatedElementUtils.hasAnnotation(method, this.annotationType); + } + } + } +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceAnnotationInterceptor.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceAnnotationInterceptor.java new file mode 100644 index 00000000..b25ed8e6 --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceAnnotationInterceptor.java @@ -0,0 +1,36 @@ + +package com.github.yiuman.citrus.support.datasource; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +/** + * DataSource注解的代理方法拦截器 + * + * @author yiuman + * @date 2020/11/30 + */ +public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor { + + private final DataSourceClassResolver dataSourceClassResolver; + + public DynamicDataSourceAnnotationInterceptor() { + dataSourceClassResolver = new DataSourceClassResolver(true); + } + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + try { + String dsKey = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis()); + if(StringUtils.isNotBlank(dsKey)){ + DynamicDataSourceHolder.push(dsKey); + } + + return invocation.proceed(); + } finally { + DynamicDataSourceHolder.poll(); + } + } + +} \ No newline at end of file diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceHolder.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceHolder.java new file mode 100644 index 00000000..9bf4e2cd --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceHolder.java @@ -0,0 +1,75 @@ +package com.github.yiuman.citrus.support.datasource; + +import org.springframework.core.NamedThreadLocal; +import org.springframework.util.StringUtils; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * 数据源切换持有者 + * 用于动态切换数据源以及多数据源事务管理 + * + * @author yiuman + * @date 2020/12/1 + */ +public final class DynamicDataSourceHolder { + + /** + * 后进先出 + */ + private final static ThreadLocal> LOOKUP_KEY_HOLDER = new NamedThreadLocal>("dynamic-datasource") { + @Override + protected Deque initialValue() { + return new ArrayDeque<>(); + } + }; + + private DynamicDataSourceHolder() { + } + + /** + * 获得当前线程数据源 + * + * @return 数据源名称 + */ + public static String peek() { + return LOOKUP_KEY_HOLDER.get().peek(); + } + + /** + * 设置当前线程数据源 + * + * 如非必要不要手动调用,调用后确保最终清除 + * + * + * @param ds 数据源名称 + */ + public static void push(String ds) { + LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds); + } + + /** + * 清空当前线程数据源 + * + * 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称 + * + */ + public static void poll() { + Deque deque = LOOKUP_KEY_HOLDER.get(); + deque.poll(); + if (deque.isEmpty()) { + LOOKUP_KEY_HOLDER.remove(); + } + } + + /** + * 强制清空本地线程 + * + * 防止内存泄漏,如手动调用了push可调用此方法确保清除 + * + */ + public static void clear() { + LOOKUP_KEY_HOLDER.remove(); + } +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceProperties.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceProperties.java new file mode 100644 index 00000000..9736c289 --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceProperties.java @@ -0,0 +1,64 @@ +package com.github.yiuman.citrus.support.datasource; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; + +/** + * 动态数据源配置 + * + * @author yiuman + * @date 2020/11/30 + */ +@ConfigurationProperties(prefix = "spring.datasource") +public class DynamicDataSourceProperties { + + /** + * 主数据源 + * 默认为spring.datasource + */ + private String primary = ""; + + /** + * 是否严格模式,若为true找不到数据源抛出异常 + */ + private boolean strict = true; + + /** + * 数据源名称与数据源配置 + */ + private Map multiples; + + public DynamicDataSourceProperties() { + } + + public String getPrimary() { + return primary; + } + + public void setPrimary(String primary) { + this.primary = primary; + } + + public boolean isStrict() { + return strict; + } + + public void setStrict(boolean strict) { + this.strict = strict; + } + + public Map getMultiples() { + return multiples; + } + + public void setMultiples(Map multiples) { + this.multiples = multiples; + } + + public Map getDatasource() { + return multiples; + } + +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicSqlSessionTemplate.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicSqlSessionTemplate.java new file mode 100644 index 00000000..416aa5f5 --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicSqlSessionTemplate.java @@ -0,0 +1,289 @@ +package com.github.yiuman.citrus.support.datasource; + +import org.apache.ibatis.cursor.Cursor; +import org.apache.ibatis.exceptions.PersistenceException; +import org.apache.ibatis.executor.BatchResult; +import org.apache.ibatis.session.*; +import org.mybatis.spring.MyBatisExceptionTranslator; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.util.List; +import java.util.Map; + +import static java.lang.reflect.Proxy.newProxyInstance; +import static org.apache.ibatis.reflection.ExceptionUtil.unwrapThrowable; +import static org.mybatis.spring.SqlSessionUtils.*; + +/** + * 动态数据源SqlSessionTemplate,拷贝原SqlSessionTemplate + * + * @author yiuman + * @date 2020/12/2 + */ +public class DynamicSqlSessionTemplate extends SqlSessionTemplate { + + /** + * 是否使用严格模式 + */ + private Boolean strict = false; + + private final SqlSessionFactory sqlSessionFactory; + + private final ExecutorType executorType; + + private final SqlSession sqlSessionProxy; + + private final PersistenceExceptionTranslator exceptionTranslator; + + private Map targetSqlSessionFactories; + + private SqlSessionFactory defaultTargetSqlSessionFactory; + + public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { + this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType()); + } + + public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) { + this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration() + .getEnvironment().getDataSource(), true)); + } + + public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, + PersistenceExceptionTranslator exceptionTranslator) { + + super(sqlSessionFactory, executorType, exceptionTranslator); + + this.sqlSessionFactory = sqlSessionFactory; + this.executorType = executorType; + this.exceptionTranslator = exceptionTranslator; + + this.sqlSessionProxy = (SqlSession) newProxyInstance( + SqlSessionFactory.class.getClassLoader(), + new Class[]{SqlSession.class}, + new SqlSessionInterceptor()); + + this.defaultTargetSqlSessionFactory = sqlSessionFactory; + } + + public void setTargetSqlSessionFactories(Map targetSqlSessionFactories) { + this.targetSqlSessionFactories = targetSqlSessionFactories; + } + + public void setDefaultTargetSqlSessionFactory(SqlSessionFactory defaultTargetSqlSessionFactory) { + this.defaultTargetSqlSessionFactory = defaultTargetSqlSessionFactory; + } + + public void setStrict(Boolean strict) { + this.strict = strict; + } + + /*** + * 获取当前使用数据源对应的会话工厂 + */ + @Override + public SqlSessionFactory getSqlSessionFactory() { + String dataSourceKey = DynamicDataSourceHolder.peek(); + SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactories.get(dataSourceKey); + + if (!StringUtils.isEmpty(dataSourceKey) && strict && targetSqlSessionFactory == null) { + throw new IllegalArgumentException(String.format("can not find DataSource %s,please check your datasource settings", dataSourceKey)); + } + + if (targetSqlSessionFactory != null) { + return targetSqlSessionFactory; + } else if (defaultTargetSqlSessionFactory != null) { + return defaultTargetSqlSessionFactory; + } else { + Assert.notNull(targetSqlSessionFactories, "Property 'targetSqlSessionFactories' or 'defaultTargetSqlSessionFactory' are required"); + Assert.notNull(defaultTargetSqlSessionFactory, "Property 'defaultTargetSqlSessionFactory' or 'targetSqlSessionFactories' are required"); + } + return this.sqlSessionFactory; + } + + + /** + * 这个方法的实现和父类的实现是基本一致的,唯一不同的就是在getSqlSession方法传参中获取会话工厂的方式 + */ + private class SqlSessionInterceptor implements InvocationHandler { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + //在getSqlSession传参时候,用我们重写的getSqlSessionFactory获取当前数据源对应的会话工厂 + final SqlSession sqlSession = getSqlSession( + DynamicSqlSessionTemplate.this.getSqlSessionFactory(), + DynamicSqlSessionTemplate.this.executorType, + DynamicSqlSessionTemplate.this.exceptionTranslator); + try { + Object result = method.invoke(sqlSession, args); + if (!isSqlSessionTransactional(sqlSession, DynamicSqlSessionTemplate.this.getSqlSessionFactory())) { + sqlSession.commit(true); + } + return result; + } catch (Throwable t) { + Throwable unwrapped = unwrapThrowable(t); + if (DynamicSqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) { + Throwable translated = DynamicSqlSessionTemplate.this.exceptionTranslator + .translateExceptionIfPossible((PersistenceException) unwrapped); + if (translated != null) { + unwrapped = translated; + } + } + throw unwrapped; + } finally { + closeSqlSession(sqlSession, DynamicSqlSessionTemplate.this.getSqlSessionFactory()); + } + } + } + + @Override + public ExecutorType getExecutorType() { + return this.executorType; + } + + @Override + public PersistenceExceptionTranslator getPersistenceExceptionTranslator() { + return this.exceptionTranslator; + } + + @Override + public T selectOne(String statement) { + return this.sqlSessionProxy.selectOne(statement); + } + + @Override + public T selectOne(String statement, Object parameter) { + return this.sqlSessionProxy.selectOne(statement, parameter); + } + + @Override + public Map selectMap(String statement, String mapKey) { + return this.sqlSessionProxy.selectMap(statement, mapKey); + } + + @Override + public Map selectMap(String statement, Object parameter, String mapKey) { + return this.sqlSessionProxy.selectMap(statement, parameter, mapKey); + } + + @Override + public Map selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) { + return this.sqlSessionProxy.selectMap(statement, parameter, mapKey, rowBounds); + } + + @Override + public Cursor selectCursor(String statement) { + return this.sqlSessionProxy.selectCursor(statement); + } + + @Override + public Cursor selectCursor(String statement, Object parameter) { + return this.sqlSessionProxy.selectCursor(statement, parameter); + } + + @Override + public Cursor selectCursor(String statement, Object parameter, RowBounds rowBounds) { + return this.sqlSessionProxy.selectCursor(statement, parameter, rowBounds); + } + + @Override + public List selectList(String statement) { + return this.sqlSessionProxy.selectList(statement); + } + + @Override + public List selectList(String statement, Object parameter) { + return this.sqlSessionProxy.selectList(statement, parameter); + } + + @Override + public List selectList(String statement, Object parameter, RowBounds rowBounds) { + return this.sqlSessionProxy.selectList(statement, parameter, rowBounds); + } + + @Override + public void select(String statement, ResultHandler handler) { + this.sqlSessionProxy.select(statement, handler); + } + + @Override + public void select(String statement, Object parameter, ResultHandler handler) { + this.sqlSessionProxy.select(statement, parameter, handler); + } + + @Override + public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { + this.sqlSessionProxy.select(statement, parameter, rowBounds, handler); + } + + @Override + public int insert(String statement) { + return this.sqlSessionProxy.insert(statement); + } + + @Override + public int insert(String statement, Object parameter) { + return this.sqlSessionProxy.insert(statement, parameter); + } + + @Override + public int update(String statement) { + return this.sqlSessionProxy.update(statement); + } + + @Override + public int update(String statement, Object parameter) { + return this.sqlSessionProxy.update(statement, parameter); + } + + @Override + public int delete(String statement) { + return this.sqlSessionProxy.delete(statement); + } + + @Override + public int delete(String statement, Object parameter) { + return this.sqlSessionProxy.delete(statement, parameter); + } + + @Override + public T getMapper(Class type) { + return getConfiguration().getMapper(type, this); + } + + @Override + public void clearCache() { + this.sqlSessionProxy.clearCache(); + } + + @Override + public Configuration getConfiguration() { + SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); + Configuration configuration = sqlSessionFactory.getConfiguration(); + //Spring初始化Bean只会给默认的一个Configuration进行Mapper扫描及注入 + // 这里如果调用了其他的数据源,是没有初始化MappedStatement的,需要从默认的进行初始化后再调用 + //不然会出现 Invalid bound statement (not found) 异常 + if (sqlSessionFactory != defaultTargetSqlSessionFactory && CollectionUtils.isEmpty(configuration.getMappedStatements())) { + defaultTargetSqlSessionFactory.getConfiguration().getMapperRegistry() + .getMappers().forEach(configuration::addMapper); + } + return configuration; + } + + @Override + public Connection getConnection() { + return this.sqlSessionProxy.getConnection(); + } + + @Override + public List flushStatements() { + return this.sqlSessionProxy.flushStatements(); + } + +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/utils/SpringUtils.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/utils/SpringUtils.java index c427a8e9..4d8217f1 100644 --- a/citrus-support/src/main/java/com/github/yiuman/citrus/support/utils/SpringUtils.java +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/utils/SpringUtils.java @@ -50,4 +50,14 @@ public static T getBean(Class tClass, String name) { return context.getBean(name, tClass); } + public static T getBean(Class tClass, String name, boolean force) { + T bean; + try { + bean = context.getBean(name, tClass); + } catch (NoSuchBeanDefinitionException ex) { + bean = force ? context.getAutowireCapableBeanFactory().createBean(tClass) : null; + } + return bean; + } + } diff --git a/citrus-system/pom.xml b/citrus-system/pom.xml index b2291065..4075b15c 100644 --- a/citrus-system/pom.xml +++ b/citrus-system/pom.xml @@ -5,7 +5,7 @@ citrus com.github.yiuman - 0.0.7 + 0.0.8 4.0.0 @@ -22,12 +22,12 @@ com.github.yiuman citrus-security - ${citrus.security.version} + ${citrus.version} com.github.yiuman citrus-support - ${citrus.security.version} + ${citrus.version} diff --git a/citrus-system/src/main/java/com/github/yiuman/citrus/system/entity/UserOrgan.java b/citrus-system/src/main/java/com/github/yiuman/citrus/system/entity/UserOrgan.java index 5b7fb889..97474223 100644 --- a/citrus-system/src/main/java/com/github/yiuman/citrus/system/entity/UserOrgan.java +++ b/citrus-system/src/main/java/com/github/yiuman/citrus/system/entity/UserOrgan.java @@ -23,7 +23,6 @@ public class UserOrgan { @TableField(exist = false) private User user; - @TableId private Long organId; @TableField(exist = false) diff --git a/pom.xml b/pom.xml index 1b7aa017..2ff97cb0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.yiuman citrus - 0.0.7 + 0.0.8 citrus springboot-开发脚手架 https://github.com/Yiuman/citrus @@ -48,15 +48,16 @@ ${java.version} 2.3.2.RELEASE 28.2-jre - 3.3.1 + 3.4.1 1.8 1.8 1.8 - 0.0.7 + 0.0.8 2.1.6 0.11.0 2.6 3.25.0-GA + 1.2.3
+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.github.yiuman.citrus.support.datasource; + +import lombok.NonNull; +import org.aopalliance.aop.Advice; +import org.springframework.aop.ClassFilter; +import org.springframework.aop.MethodMatcher; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.AopUtils; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.StaticMethodMatcher; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.Assert; + +import javax.annotation.Nonnull; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * DataSource注解的切面 + * + * @author yiuman + * @date 2020/11/30 + */ +public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { + + private final Advice advice; + + private final Pointcut pointcut; + + public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) { + this.advice = dynamicDataSourceAnnotationInterceptor; + this.pointcut = buildPointcut(); + } + + @Nonnull + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + + @Nonnull + @Override + public Advice getAdvice() { + return this.advice; + } + + @Override + public void setBeanFactory(@Nonnull BeanFactory beanFactory) throws BeansException { + if (this.advice instanceof BeanFactoryAware) { + ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory); + } + } + + private Pointcut buildPointcut() { + Pointcut cpc = new AnnotationMatchingPointcut(DataSource.class, true); + Pointcut mpc = new AnnotationMethodPoint(DataSource.class); + return new ComposablePointcut(cpc).union(mpc); + } + + /** + * In order to be compatible with the spring lower than 5.0 + */ + private static class AnnotationMethodPoint implements Pointcut { + + private final Class extends Annotation> annotationType; + + public AnnotationMethodPoint(Class extends Annotation> annotationType) { + Assert.notNull(annotationType, "Annotation type must not be null"); + this.annotationType = annotationType; + } + + @Nonnull + @Override + public ClassFilter getClassFilter() { + return ClassFilter.TRUE; + } + + @Nonnull + @Override + public MethodMatcher getMethodMatcher() { + return new AnnotationMethodMatcher(annotationType); + } + + private static class AnnotationMethodMatcher extends StaticMethodMatcher { + private final Class extends Annotation> annotationType; + + public AnnotationMethodMatcher(Class extends Annotation> annotationType) { + this.annotationType = annotationType; + } + + @Override + public boolean matches(@Nonnull Method method, @Nonnull Class> targetClass) { + if (matchesMethod(method)) { + return true; + } + // Proxy classes never have annotations on their redeclared methods. + if (Proxy.isProxyClass(targetClass)) { + return false; + } + // The method may be on an interface, so let's check on the target class as well. + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + return (specificMethod != method && matchesMethod(specificMethod)); + } + + private boolean matchesMethod(Method method) { + return AnnotatedElementUtils.hasAnnotation(method, this.annotationType); + } + } + } +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceAnnotationInterceptor.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceAnnotationInterceptor.java new file mode 100644 index 00000000..b25ed8e6 --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceAnnotationInterceptor.java @@ -0,0 +1,36 @@ + +package com.github.yiuman.citrus.support.datasource; + +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +/** + * DataSource注解的代理方法拦截器 + * + * @author yiuman + * @date 2020/11/30 + */ +public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor { + + private final DataSourceClassResolver dataSourceClassResolver; + + public DynamicDataSourceAnnotationInterceptor() { + dataSourceClassResolver = new DataSourceClassResolver(true); + } + + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + try { + String dsKey = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis()); + if(StringUtils.isNotBlank(dsKey)){ + DynamicDataSourceHolder.push(dsKey); + } + + return invocation.proceed(); + } finally { + DynamicDataSourceHolder.poll(); + } + } + +} \ No newline at end of file diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceHolder.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceHolder.java new file mode 100644 index 00000000..9bf4e2cd --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceHolder.java @@ -0,0 +1,75 @@ +package com.github.yiuman.citrus.support.datasource; + +import org.springframework.core.NamedThreadLocal; +import org.springframework.util.StringUtils; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * 数据源切换持有者 + * 用于动态切换数据源以及多数据源事务管理 + * + * @author yiuman + * @date 2020/12/1 + */ +public final class DynamicDataSourceHolder { + + /** + * 后进先出 + */ + private final static ThreadLocal> LOOKUP_KEY_HOLDER = new NamedThreadLocal>("dynamic-datasource") { + @Override + protected Deque initialValue() { + return new ArrayDeque<>(); + } + }; + + private DynamicDataSourceHolder() { + } + + /** + * 获得当前线程数据源 + * + * @return 数据源名称 + */ + public static String peek() { + return LOOKUP_KEY_HOLDER.get().peek(); + } + + /** + * 设置当前线程数据源 + * + * 如非必要不要手动调用,调用后确保最终清除 + * + * + * @param ds 数据源名称 + */ + public static void push(String ds) { + LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds); + } + + /** + * 清空当前线程数据源 + * + * 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称 + * + */ + public static void poll() { + Deque deque = LOOKUP_KEY_HOLDER.get(); + deque.poll(); + if (deque.isEmpty()) { + LOOKUP_KEY_HOLDER.remove(); + } + } + + /** + * 强制清空本地线程 + * + * 防止内存泄漏,如手动调用了push可调用此方法确保清除 + * + */ + public static void clear() { + LOOKUP_KEY_HOLDER.remove(); + } +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceProperties.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceProperties.java new file mode 100644 index 00000000..9736c289 --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicDataSourceProperties.java @@ -0,0 +1,64 @@ +package com.github.yiuman.citrus.support.datasource; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; + +/** + * 动态数据源配置 + * + * @author yiuman + * @date 2020/11/30 + */ +@ConfigurationProperties(prefix = "spring.datasource") +public class DynamicDataSourceProperties { + + /** + * 主数据源 + * 默认为spring.datasource + */ + private String primary = ""; + + /** + * 是否严格模式,若为true找不到数据源抛出异常 + */ + private boolean strict = true; + + /** + * 数据源名称与数据源配置 + */ + private Map multiples; + + public DynamicDataSourceProperties() { + } + + public String getPrimary() { + return primary; + } + + public void setPrimary(String primary) { + this.primary = primary; + } + + public boolean isStrict() { + return strict; + } + + public void setStrict(boolean strict) { + this.strict = strict; + } + + public Map getMultiples() { + return multiples; + } + + public void setMultiples(Map multiples) { + this.multiples = multiples; + } + + public Map getDatasource() { + return multiples; + } + +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicSqlSessionTemplate.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicSqlSessionTemplate.java new file mode 100644 index 00000000..416aa5f5 --- /dev/null +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/datasource/DynamicSqlSessionTemplate.java @@ -0,0 +1,289 @@ +package com.github.yiuman.citrus.support.datasource; + +import org.apache.ibatis.cursor.Cursor; +import org.apache.ibatis.exceptions.PersistenceException; +import org.apache.ibatis.executor.BatchResult; +import org.apache.ibatis.session.*; +import org.mybatis.spring.MyBatisExceptionTranslator; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.util.List; +import java.util.Map; + +import static java.lang.reflect.Proxy.newProxyInstance; +import static org.apache.ibatis.reflection.ExceptionUtil.unwrapThrowable; +import static org.mybatis.spring.SqlSessionUtils.*; + +/** + * 动态数据源SqlSessionTemplate,拷贝原SqlSessionTemplate + * + * @author yiuman + * @date 2020/12/2 + */ +public class DynamicSqlSessionTemplate extends SqlSessionTemplate { + + /** + * 是否使用严格模式 + */ + private Boolean strict = false; + + private final SqlSessionFactory sqlSessionFactory; + + private final ExecutorType executorType; + + private final SqlSession sqlSessionProxy; + + private final PersistenceExceptionTranslator exceptionTranslator; + + private Map targetSqlSessionFactories; + + private SqlSessionFactory defaultTargetSqlSessionFactory; + + public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { + this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType()); + } + + public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) { + this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration() + .getEnvironment().getDataSource(), true)); + } + + public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, + PersistenceExceptionTranslator exceptionTranslator) { + + super(sqlSessionFactory, executorType, exceptionTranslator); + + this.sqlSessionFactory = sqlSessionFactory; + this.executorType = executorType; + this.exceptionTranslator = exceptionTranslator; + + this.sqlSessionProxy = (SqlSession) newProxyInstance( + SqlSessionFactory.class.getClassLoader(), + new Class[]{SqlSession.class}, + new SqlSessionInterceptor()); + + this.defaultTargetSqlSessionFactory = sqlSessionFactory; + } + + public void setTargetSqlSessionFactories(Map targetSqlSessionFactories) { + this.targetSqlSessionFactories = targetSqlSessionFactories; + } + + public void setDefaultTargetSqlSessionFactory(SqlSessionFactory defaultTargetSqlSessionFactory) { + this.defaultTargetSqlSessionFactory = defaultTargetSqlSessionFactory; + } + + public void setStrict(Boolean strict) { + this.strict = strict; + } + + /*** + * 获取当前使用数据源对应的会话工厂 + */ + @Override + public SqlSessionFactory getSqlSessionFactory() { + String dataSourceKey = DynamicDataSourceHolder.peek(); + SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactories.get(dataSourceKey); + + if (!StringUtils.isEmpty(dataSourceKey) && strict && targetSqlSessionFactory == null) { + throw new IllegalArgumentException(String.format("can not find DataSource %s,please check your datasource settings", dataSourceKey)); + } + + if (targetSqlSessionFactory != null) { + return targetSqlSessionFactory; + } else if (defaultTargetSqlSessionFactory != null) { + return defaultTargetSqlSessionFactory; + } else { + Assert.notNull(targetSqlSessionFactories, "Property 'targetSqlSessionFactories' or 'defaultTargetSqlSessionFactory' are required"); + Assert.notNull(defaultTargetSqlSessionFactory, "Property 'defaultTargetSqlSessionFactory' or 'targetSqlSessionFactories' are required"); + } + return this.sqlSessionFactory; + } + + + /** + * 这个方法的实现和父类的实现是基本一致的,唯一不同的就是在getSqlSession方法传参中获取会话工厂的方式 + */ + private class SqlSessionInterceptor implements InvocationHandler { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + //在getSqlSession传参时候,用我们重写的getSqlSessionFactory获取当前数据源对应的会话工厂 + final SqlSession sqlSession = getSqlSession( + DynamicSqlSessionTemplate.this.getSqlSessionFactory(), + DynamicSqlSessionTemplate.this.executorType, + DynamicSqlSessionTemplate.this.exceptionTranslator); + try { + Object result = method.invoke(sqlSession, args); + if (!isSqlSessionTransactional(sqlSession, DynamicSqlSessionTemplate.this.getSqlSessionFactory())) { + sqlSession.commit(true); + } + return result; + } catch (Throwable t) { + Throwable unwrapped = unwrapThrowable(t); + if (DynamicSqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) { + Throwable translated = DynamicSqlSessionTemplate.this.exceptionTranslator + .translateExceptionIfPossible((PersistenceException) unwrapped); + if (translated != null) { + unwrapped = translated; + } + } + throw unwrapped; + } finally { + closeSqlSession(sqlSession, DynamicSqlSessionTemplate.this.getSqlSessionFactory()); + } + } + } + + @Override + public ExecutorType getExecutorType() { + return this.executorType; + } + + @Override + public PersistenceExceptionTranslator getPersistenceExceptionTranslator() { + return this.exceptionTranslator; + } + + @Override + public T selectOne(String statement) { + return this.sqlSessionProxy.selectOne(statement); + } + + @Override + public T selectOne(String statement, Object parameter) { + return this.sqlSessionProxy.selectOne(statement, parameter); + } + + @Override + public Map selectMap(String statement, String mapKey) { + return this.sqlSessionProxy.selectMap(statement, mapKey); + } + + @Override + public Map selectMap(String statement, Object parameter, String mapKey) { + return this.sqlSessionProxy.selectMap(statement, parameter, mapKey); + } + + @Override + public Map selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) { + return this.sqlSessionProxy.selectMap(statement, parameter, mapKey, rowBounds); + } + + @Override + public Cursor selectCursor(String statement) { + return this.sqlSessionProxy.selectCursor(statement); + } + + @Override + public Cursor selectCursor(String statement, Object parameter) { + return this.sqlSessionProxy.selectCursor(statement, parameter); + } + + @Override + public Cursor selectCursor(String statement, Object parameter, RowBounds rowBounds) { + return this.sqlSessionProxy.selectCursor(statement, parameter, rowBounds); + } + + @Override + public List selectList(String statement) { + return this.sqlSessionProxy.selectList(statement); + } + + @Override + public List selectList(String statement, Object parameter) { + return this.sqlSessionProxy.selectList(statement, parameter); + } + + @Override + public List selectList(String statement, Object parameter, RowBounds rowBounds) { + return this.sqlSessionProxy.selectList(statement, parameter, rowBounds); + } + + @Override + public void select(String statement, ResultHandler handler) { + this.sqlSessionProxy.select(statement, handler); + } + + @Override + public void select(String statement, Object parameter, ResultHandler handler) { + this.sqlSessionProxy.select(statement, parameter, handler); + } + + @Override + public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { + this.sqlSessionProxy.select(statement, parameter, rowBounds, handler); + } + + @Override + public int insert(String statement) { + return this.sqlSessionProxy.insert(statement); + } + + @Override + public int insert(String statement, Object parameter) { + return this.sqlSessionProxy.insert(statement, parameter); + } + + @Override + public int update(String statement) { + return this.sqlSessionProxy.update(statement); + } + + @Override + public int update(String statement, Object parameter) { + return this.sqlSessionProxy.update(statement, parameter); + } + + @Override + public int delete(String statement) { + return this.sqlSessionProxy.delete(statement); + } + + @Override + public int delete(String statement, Object parameter) { + return this.sqlSessionProxy.delete(statement, parameter); + } + + @Override + public T getMapper(Class type) { + return getConfiguration().getMapper(type, this); + } + + @Override + public void clearCache() { + this.sqlSessionProxy.clearCache(); + } + + @Override + public Configuration getConfiguration() { + SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); + Configuration configuration = sqlSessionFactory.getConfiguration(); + //Spring初始化Bean只会给默认的一个Configuration进行Mapper扫描及注入 + // 这里如果调用了其他的数据源,是没有初始化MappedStatement的,需要从默认的进行初始化后再调用 + //不然会出现 Invalid bound statement (not found) 异常 + if (sqlSessionFactory != defaultTargetSqlSessionFactory && CollectionUtils.isEmpty(configuration.getMappedStatements())) { + defaultTargetSqlSessionFactory.getConfiguration().getMapperRegistry() + .getMappers().forEach(configuration::addMapper); + } + return configuration; + } + + @Override + public Connection getConnection() { + return this.sqlSessionProxy.getConnection(); + } + + @Override + public List flushStatements() { + return this.sqlSessionProxy.flushStatements(); + } + +} diff --git a/citrus-support/src/main/java/com/github/yiuman/citrus/support/utils/SpringUtils.java b/citrus-support/src/main/java/com/github/yiuman/citrus/support/utils/SpringUtils.java index c427a8e9..4d8217f1 100644 --- a/citrus-support/src/main/java/com/github/yiuman/citrus/support/utils/SpringUtils.java +++ b/citrus-support/src/main/java/com/github/yiuman/citrus/support/utils/SpringUtils.java @@ -50,4 +50,14 @@ public static T getBean(Class tClass, String name) { return context.getBean(name, tClass); } + public static T getBean(Class tClass, String name, boolean force) { + T bean; + try { + bean = context.getBean(name, tClass); + } catch (NoSuchBeanDefinitionException ex) { + bean = force ? context.getAutowireCapableBeanFactory().createBean(tClass) : null; + } + return bean; + } + } diff --git a/citrus-system/pom.xml b/citrus-system/pom.xml index b2291065..4075b15c 100644 --- a/citrus-system/pom.xml +++ b/citrus-system/pom.xml @@ -5,7 +5,7 @@ citrus com.github.yiuman - 0.0.7 + 0.0.8 4.0.0 @@ -22,12 +22,12 @@ com.github.yiuman citrus-security - ${citrus.security.version} + ${citrus.version} com.github.yiuman citrus-support - ${citrus.security.version} + ${citrus.version} diff --git a/citrus-system/src/main/java/com/github/yiuman/citrus/system/entity/UserOrgan.java b/citrus-system/src/main/java/com/github/yiuman/citrus/system/entity/UserOrgan.java index 5b7fb889..97474223 100644 --- a/citrus-system/src/main/java/com/github/yiuman/citrus/system/entity/UserOrgan.java +++ b/citrus-system/src/main/java/com/github/yiuman/citrus/system/entity/UserOrgan.java @@ -23,7 +23,6 @@ public class UserOrgan { @TableField(exist = false) private User user; - @TableId private Long organId; @TableField(exist = false) diff --git a/pom.xml b/pom.xml index 1b7aa017..2ff97cb0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.yiuman citrus - 0.0.7 + 0.0.8 citrus springboot-开发脚手架 https://github.com/Yiuman/citrus @@ -48,15 +48,16 @@ ${java.version} 2.3.2.RELEASE 28.2-jre - 3.3.1 + 3.4.1 1.8 1.8 1.8 - 0.0.7 + 0.0.8 2.1.6 0.11.0 2.6 3.25.0-GA + 1.2.3
+ * 如非必要不要手动调用,调用后确保最终清除 + *
+ * 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称 + *
+ * 防止内存泄漏,如手动调用了push可调用此方法确保清除 + *