前序
系列的前三篇详细描述此项目由搭建至引入AOP的步骤,现在已经能正确获取当前dao层操作的Spring上下文的bean对象,最后就是完成对基础增删改查的SQL条件的简单构造,完成后就实现了CommonMapper的功能,更复杂的功能可以以此类推,按照基础代码添加需要的逻辑即可。
上一篇文章:# 实现Mybatis-CommonMapper通用增删改查功能记录(三-引入编程式AOP)
CommonMapper
根据前几篇文章下来,中间对代码进行了一些修改重构,目前CommonMapper完成增删改查的基础方法。
java
package org.nott.mybatis.provider;
import org.nott.mybatis.model.MybatisSqlBean;
import org.nott.mybatis.sql.builder.DeleteSqlConditionBuilder;
import org.nott.mybatis.sql.builder.SqlBuilder;
import org.nott.mybatis.support.aop.ConCurrentMapperAopFactory;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@Component
@Scope("prototype")
public interface CommonMapper<T> {
@SelectProvider(type = BaseSelectProvider.class, method = "selectById")
T selectById(@Param("id") Serializable id);
@SelectProvider(type = BaseSelectProvider.class, method = "selectOne")
T selectOne();
@SelectProvider(type = BaseSelectProvider.class, method = "selectList")
List<T> selectList();
@SelectProvider(type = BaseSelectProvider.class, method = "selectListByCondition")
List<T> selectListByCondition(SqlConditionBuilder querySqlConditionBuilder);
@SelectProvider(type = BaseSelectProvider.class, method = "pageCount")
Long count();
@SelectProvider(type = BaseSelectProvider.class, method = "pageCountByCondition")
Long countByCondition(SqlConditionBuilder querySqlConditionBuilder);
省略...
由上面的代码不难看出,基础的方法都依赖于@xxProvider提供的类和方法名来定位具体的SQL语句获取方法,以下简要用BaseSelectProvider作为例子说明一下流程。
java
package org.nott.mybatis.provider;
import org.nott.mybatis.model.MybatisSqlBean;
import org.nott.mybatis.sql.builder.QuerySqlConditionBuilder;
import org.nott.mybatis.sql.builder.SqlBuilder;
import org.nott.mybatis.support.aop.ConCurrentMapperAopFactory;
import java.io.Serializable;
import java.util.Map;
/**
* 基础 mybatis selectProvider
*/
public class BaseSelectProvider {
public static String selectById(Map<String, Serializable> map) {
Serializable id = map.get("id");
MybatisSqlBean bean = ConCurrentMapperAopFactory.getBean();
return SqlBuilder.buildFindByPkSql(bean, id);
}
...
}
这里放出了selectById方法作为例子,顾名思义是按照id查找实体方法,定义了MybatisSqlBean 作为数据库对象信息存放的实体,存放操作对象对应的表名、字段、主键、class等信息 。定义的ConCurrentMapperAopFactory 负责获取MybatisAopInterceptor 存放的当前线程操作信息,将它封装到MybatisSqlBean实体里的工厂对象。得到bean信息最后使用SqlBuilder完成mybatis SQL构造。
MybatisSqlBean
java
@Data
@Builder
public class MybatisSqlBean {
/**
* 表名
*/
private String tableName;
/**
* 字段集合
*/
private List<String> tableColums;
/**
* 主键
*/
private Pk pk;
/**
* bean
*/
private Object originalBean;
/**
* bean的Class
*/
private Class<?> originalBeanClass;
}
ConCurrentMapperAopFactory
java
public class ConCurrentMapperAopFactory {
public ConCurrentMapperAopFactory() {
}
public static final ConCurrentMapperAopFactory FACTORY = new ConCurrentMapperAopFactory();
/**
* mapper与所属泛型的map
*/
public static final Map<String,Class<?>> mapperGenericClassMap = new ConcurrentHashMap<>();
public static MybatisSqlBean getBean(){
return FACTORY.getCurrentMapperBean();
}
...具体操作
}
SqlBuilder
SqlBuilder实际上是对mybatis的SQL语句构建器的封装,基础用法如下,SqlBuilder内部按照条件对mybatis的SQL对象进行修改。
java
public String deletePersonSql() {
return new SQL() {{
DELETE_FROM("PERSON");
WHERE("ID = #{id}");
}}.toString();
}
// 所以selectById实际上构建的语句如下
public String selectById() {
return new SQL() {{
FORM("user");
WHERE("id = #{id}");
}}.toString();
}
// 封装实现
public static String buildFindByPkSql(MybatisSqlBean bean, Serializable value) {
String valStr = value.toString();
Pk pk = bean.getPk();
QuerySqlConditionBuilder builder = QuerySqlConditionBuilder.build();
builder.setSqlConditions(Arrays.asList(SqlConditions.basicBuilder().value(valStr).colum(pk.getName()).build()));
return buildSql(bean, builder);
}
基于CommonMapper的数据操作流向:
CommonMapper -> BaseSelectProvider -> ConCurrentMapperAopFactory -> SqlBuilder
动态数据源
找到org.springframework.jdbc.datasource.lookup 包下的AbstractRoutingDataSource抽象类,根据名称可以大致得知它的作用是路由数据源的。
部分源码如下所示,需要定义自定义的数据源类提供成员变量值和重写determineCurrentLookupKey抽象方法
java
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
public AbstractRoutingDataSource() {
}
@Nullable
protected abstract Object determineCurrentLookupKey();
新增DynamicDataSourceHolder类,存放当前线程操作数据源的key。
java
public class DynamicDataSourceHolder {
private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();
public static void setDynamicDataSourceKey(String key){
DYNAMIC_DATASOURCE_KEY.set(key);
}
public static String getDynamicDataSourceKey(){
String key = DYNAMIC_DATASOURCE_KEY.get();
return key == null ? "default" : key;
}
编写动态数据源类,继承AbstractRoutingDataSource,重写获取determineCurrentLookupKey方法,标识获取数据源的方式。
java
@Getter
@Setter
public class DynamicDataSource extends AbstractRoutingDataSource {
private Map<Object, Object> defineTargetDataSources;
/**
* 重写AbstractRoutingDataSource的determineCurrentLookupKey,定义选中当前数据源key的方法
* @return DataSource's key
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDynamicDataSourceKey();
}
}
现在需要的工作是在配置文件中获取数据源列表配置内容,遍历列表将每个数据源作为bean注册到Spring上下文中,后续按需获取。
刚开始的思路是在项目启动时加载application.yml中的内容,作为配置类,但在启动时读取的内容为空,没做到动态注册DataSource bean,后续的做法是加入snakeyaml依赖,读取名为data-source.yml的自定义配置文件,把内容set进入bean中,加载到上下文中,后续通过name属性获取bean。
yml
dataSourceConfigs:
- name: mysql-db01
url:
username: root
password:
driverClassName: com.mysql.cj.jdbc.Driver
# 标识默认数据源
primary: true
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimumIdle: 0
maximumPoolSize: 20
idleTimeout: 10000
connectionTestQuery: select 1
poolName: hikari-01
编写MultiplyDataSourceSupport,实现BeanDefinitionRegistryPostProcessor接口来完成bean注册到Spring容器的作用。
java
@Configuration
public class MultiplyDataSourceSupport implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
MultiplyDataSourceConfig multiplyDataSourceConfig = loadDataSourceConfigs();
if (multiplyDataSourceConfig == null || multiplyDataSourceConfig.getDataSourceConfigs() == null) {
throw new DynamicInitException("Dont found any dynamic data source config info.");
}
// 遍历配置类
for (DataSourceConfig properties : multiplyDataSourceConfig.getDataSourceConfigs()) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DataSource.class);
// 组装DataSource对象
DataSource dataSource = DataSourceConfigUtils.createDataSource(properties);
beanDefinition.setInstanceSupplier(() -> dataSource);
registry.registerBeanDefinition(properties.getName(), beanDefinition);
if (properties.isPrimary()) {
registry.registerBeanDefinition(DataSourceConstant.DEFAULT_DB, beanDefinition);
}
}
}
public MultiplyDataSourceConfig loadDataSourceConfigs() {
// 加载yml内容,省略
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// do nothing
}
}
最后一步编写动态数据源加载时的配置类,把以上已经装入Spring容器的DataSource列表bean全部装载到DynamicDataSource继承的成员变量中。
java
@Configuration
@RequiredArgsConstructor
public class MultiplyDataSourceContextConfiguration {
@Autowired
private BeanFactory beanFactory;
@Bean
@Primary
public DynamicDataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
MultiplyDataSourceConfig multiplyDataSourceConfig = yamlDataSourceInfoProvider().getMultiplyDataSourceConfig();
dynamicDataSource.setDefaultTargetDataSource(beanFactory.getBean(DataSourceConstant.DEFAULT_DB));
if (multiplyDataSourceConfig == null) {
return dynamicDataSource;
}
Map<Object, Object> defineTargetDataSources = new HashMap<>(16);
List<DataSourceConfig> dataSourceConfigs = multiplyDataSourceConfig.getDataSourceConfigs();
for (DataSourceConfig config : dataSourceConfigs) {
Object bean = beanFactory.getBean(config.getName());
defineTargetDataSources.put(config.getName(), bean);
}
dynamicDataSource.setDefaultTargetDataSource(beanFactory.getBean(DataSourceConstant.DEFAULT_DB));
dynamicDataSource.setTargetDataSources(defineTargetDataSources);
dynamicDataSource.setDefineTargetDataSources(defineTargetDataSources);
return dynamicDataSource;
}
}
目前配置完可以通过DynamicDataSourceHolder来切换数据源。
java
public void test(){
// 方法1
doSomethingOnDb01(); DynamicDataSourceHolder.setDynamicDataSourceKey(name);
// 方法2
doSomethingOnDb02();
}
同样可以通过定义注解,在方法执行前通过aop切面方式,切换数据源。
java
@Aspect
@Component
public class ChangeDataSourceInterceptor {
@Around("@annotation(org.nott.datasource.annotations.DataSource)")
public Object around(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
String value = DataSourceConstant.DEFAULT_DB;
boolean isCustomDataSource = method.isAnnotationPresent(DataSource.class);
if (isCustomDataSource) {
DataSource annotation = method.getAnnotation(DataSource.class);
value = annotation.value();
}
DynamicDataSourceHolder.setDynamicDataSourceKey(value);
Object result;
try {
result = point.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
return result;
}
}
测试
将以下两条MySql语句分别插入到test和test01数据库中。
mysql
INSERT INTO `test`.`user` (`id`, `age`, `email`, `name`, `password`) VALUES ('410544b2-4001-4271-9855-fec4b62350b', 15, 'power@win.com', 'mybatis-test', '$10$xupd3MaYf1fA2wMjaNl6AuTyBE2ifOGd2xbUVpdV1fgqYyBi9XiRy');
INSERT INTO `test01`.`user` (`id`, `age`, `email`, `name`, `password`) VALUES ('410544b2-4001-4271-9855-fec4b62350b', 15, 'test01@win.com', 'mybatis-test01', '$10$xupd3MaYf1fA2wMjaNl6AuTyBE2ifOGd2xbUVpdV1fgqYyBi9XiRy');
java
// 本来用junit测试方法,不起作用后换成REST API测试
@RequestMapping("/test")
public void test() {
User one = userMapper.selectOneByCondition(QuerySqlConditionBuilder.build().eq("id", "410544b2-4001-4271-9855-fec4b62350b"));
DynamicDataSourceHolder.setDynamicDataSourceKey("mysql-db02");
User two = userMapper.selectOneByCondition(QuerySqlConditionBuilder.build().eq("id", "410544b2-4001-4271-9855-fec4b62350b"));
Assert.isTrue("mybatis-test".equals(one.getName()),"");
Assert.isTrue("mybatis-test01".equals(two.getName()),"");
}
程序中的断言不抛出异常,且日志输出类似于c.z.h.HikariDataSource -hikari-01/02的语句,视为成功。
小结
这一系列的几篇文章篇幅有意压缩后还是很长,受限于篇幅,中间有些概念点没法深挖,造成读者观感不好,所以是CommonMapper话题的最后一篇,后续再挖掘其他有意思的干货学习分享。