动态数据源切换

(本文基于Springboot+MyBatis)

背景:业务数据和其他业务的数据都在一个库中,而库中的表也很多,因此需要为自己的业务独立建库,并在新库中创建和老库中结构相同的业务表(自己业务使用到的表)。

整体流程:

  1. 用户 -> 新库: 新建业务表
  2. 老库 -> 新库: 同步数据
  3. 用户 –> 新库: 暂停新库写操作
  4. 新库 -> 老库: 反向同步数据
  5. 用户 -> 新库: 恢复新库写操作

1.方案

在Mapper层接口方法执行前进行动态数据源的切换。即使用AOP切面,切点是Mapper接口中的所有方法,并在mapper层的AOP切面中修改线程变量进行数据源切换。

2.实现步骤

2.1 数据源配置

    <!-- 老库数据源1的配置 -->
    <bean id="dataSource1" class="com.zaxxer.hikari.HikariDataSource">
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test1"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    </bean>
    <bean id="ds1SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource1" />
        <property name="configLocation" value="classpath:com/example/demo/common/dal/mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath:com/example/demo/common/dal/ds1/mapper/*Mapper.xml"/>
    </bean>
    <bean id="ds1SqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="ds1SqlSessionFactory"/>
    </bean>

    <!-- 新库数据源2的配置 -->
    <bean id="dataSource2" class="com.zaxxer.hikari.HikariDataSource">
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test2"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    </bean>
    <bean id="ds2SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource2" />
        <property name="configLocation" value="classpath:com/example/demo/common/dal/mybatis-config.xml"/>
        <!-- 这里使用通配符是因为要通用ds1的mapper -->
        <property name="mapperLocations" value="classpath:com/example/demo/common/dal/**/mapper/*Mapper.xml"/>
    </bean>
    <bean id="ds2SqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="ds2SqlSessionFactory"/>
    </bean>

2.2 通用代码部分

import lombok.Getter;

@Getter
public enum DataSourceType {
    DS_1("dataSource1"),
    DS_2("dataSource2");

    private String value;
    DataSourceType(String value) {
        this.value = value;
    }

    public static DataSourceType getByValue(String value) {
        for (DataSourceType type : DataSourceType.values()) {
            if (type.value.equals(value)) {
                return type;
            }
        }
        return DS_1;
    }
}

// 线程变量管理
public class DynamicDataSourceContextHolder {

    private static final ThreadLocal<DataSourceType> CURRENT_MAPPER_CONTEXT = new ThreadLocal<>();

    public static void setCurrentMapperType(DataSourceType dataSourceType) {
        CURRENT_MAPPER_CONTEXT.set(dataSourceType);
    }

    public static DataSourceType getCurrentMapperType() {
        return CURRENT_MAPPER_CONTEXT.get();
    }

    public static void clearCurrentMapperType() {
        CURRENT_MAPPER_CONTEXT.remove();
    }
}

2.3 实现动态数据源

动态数据源需要实现org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource抽象类,并实现其determineCurrentLookupKey方法。

import com.google.common.base.MoreObjects;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;


@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
    private static Map<Object,Object> dataSourceMap = new HashMap<Object, Object>();

    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        dataSourceMap.putAll(targetDataSources);
        super.afterPropertiesSet();// 必须添加该句,否则新添加数据源无法识别到
    }

    @Override
    protected Object determineCurrentLookupKey() {
        // Mapper层方法切面设值
        DataSourceType currentMapper = DynamicDataSourceContextHolder.getCurrentMapperType();
        if (Objects.isNull(currentMapper)) {
            // 默认使用老库的数据源
            log.info("determineCurrentLookupKey, dataSourceType: {}", DataSourceType.DS_1);
            return null;
        }

        // 如果currentMapper不为空,则使用currentMapper
        log.info("determineCurrentLookupKey, dataSourceType:{}", currentMapper);
        return dataSourceType1.getValue();
    }
}

2.4 配置动态数据源

将多个数据源都配置到动态数据源中。并将SqlSessionFactory、SqlSessionTemplate和PlatformTransactionManager都指向动态数据源的实例。

import com.google.common.collect.Lists;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class DynamicDataSourceConfig {
    // 将多个数据源统一放置到动态数据源中
    @Primary
    @Bean("dynamicDataSource")
    public DynamicDataSource dynamicDataSource(@Qualifier("dataSource1") DataSource master,
                                    @Qualifier("dataSource2") DataSource slave) {
        DynamicDataSource ds = new DynamicDataSource();
        // 设置默认数据源
        ds.setDefaultTargetDataSource(master);
        // 设置数据源集合
        Map<Object, Object> dataSourceList = new HashMap<>();
        // 旧数据源
        dataSourceList.put("dataSource1", master);
        // 新数据源
        dataSourceList.put("dataSource2", slave);
        ds.setTargetDataSources(dataSourceList);
        return ds;
    }

    // 将SqlSessionFactory指向动态数据源
    @Bean("dynamicSqlSessionFactory")
    public SqlSessionFactory dynamicSqlSessionFactory(@Qualifier("dynamicDataSource")
                                                                  DynamicDataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
        fb.setDataSource(dynamicDataSource);
        // 设置Mybatis的配置文件
        fb.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:com/example/demo/common/dal/mybatis-config.xml"));
        // 设置Mapper XML文件的位置
        List<Resource> resources = Lists.newArrayList();
        resources.add(new PathMatchingResourcePatternResolver().getResource("classpath:com/example/demo/common/dal/ds1/mapper/*Mapper.xml"));
        resources.add(new PathMatchingResourcePatternResolver().getResource("classpath:com/example/demo/common/dal/ds2/mapper/*Mapper.xml"));
        fb.setMapperLocations(resources.toArray(new Resource[resources.size()]));
        //返回对象
        return fb.getObject();
    }

    // SqlSession
    @Bean("dynamicSqlSessionTemplate")
    public SqlSessionTemplate dynamicSqlSessionTemplate(@Qualifier("dynamicSqlSessionFactory")
                                                                SqlSessionFactory dynamicSqlSessionFactory) {
        return new  SqlSessionTemplate(dynamicSqlSessionFactory);
    }

    // 扫描指定包路径下的Mapper接口,并注册成Spring Bean,并绑定到数据源上
    // BeanDefinitionRegistryPostProcessor:Mybatis-spring的后置处理器
    // 推荐使用 @MapperScan 方式,简洁
    @Bean
    public MapperScannerConfigurer mapperScannerCOnfigurer() {
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        // 指定mapper接口的包路径,多个时使用逗号分隔
        msc.setBasePackage("com.example.demo.common.dal.ds1.mapper,com.example.demo.common.dal.ds2.mapper");
        // 绑定数据源
        msc.setSqlSessionTemplateBeanName("dynamicSqlSessionTemplate");
        return msc;
    }

    // 主事务管理器
    @Primary
    @Bean(name="dynamicTransactionManager")
    public PlatformTransactionManager dynamicTransactionManager(@Qualifier("dynamicDataSource")
                                                                            DynamicDataSource dynamicDataSource) {
        DataSourceTransactionManager mg = new DataSourceTransactionManager(dynamicDataSource);
        mg.setNestedTransactionAllowed(true);
        return mg;
    }
}

2.5 Mapper层切面

import com.example.demo.common.dal.config.DataSourceType;
import com.example.demo.common.dal.config.DynamicDataSourceContextHolder;
import com.example.demo.common.dal.config.MultiSqlSessionManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Slf4j
@Aspect
@Component
public class MapperTestAspect {
    @Pointcut("execution(* com.example.demo.common.dal.ds1.mapper.*.*(..))")
    public void mapperInterface1() {
    }

    @Pointcut("execution(* com.example.demo.common.dal.ds2.mapper.*.*(..))")
    public void mapperInterface2() {
    }

    private final Map<String, String> mapps = new HashMap(){{
        put("com.example.demo.common.dal.ds1.mapper.StudentInfoMapper", "dataSource1");
        put("com.example.demo.common.dal.ds2.mapper.TeacherInfoMapper", "dataSource2");
    }};
    
    @Around("mapperInterface1() || mapperInterface2()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String mapperClassName = method.getDeclaringClass().getName();
        DataSourceType currentMapper = DynamicDataSourceContextHolder.getCurrentMapperType();
        // 根据Mapper接口的全限定名获取对应的数据源名称,动态配置中获取,Nacos或drm
        String dataSourceName = mapps.get(mapperClassName);
        DataSourceType dataSourceType = DataSourceType.getByValue(dataSourceName);
        log.info("当前使用的数据源,currentMapperDataSource={},toMapperDataSource:{},mapperClassName:{}#{}", currentMapper,
                Objects.isNull(dataSourceType) ? "null" : dataSourceType.getValue(), mapperClassName, method.getName());
        try {
            DynamicDataSourceContextHolder.setCurrentMapperType(dataSourceType);
            return joinPoint.proceed();
        } finally {
            DynamicDataSourceContextHolder.clearCurrentMapperType();
        }
    }
}

以上代码在无事务操作的方法中,可以做到随时切换数据源操作

3. 事务中切换数据源

使用上面的代码时虽然日志会显示切换了数据源但其实并没有,这是由于事务开启时已经绑定了数据源,并调用了DataSource.getConnection()方法,将数据源绑定到了当前线程。要解决这个怎么整呢?下文讲述了一种不使用分布式事务的方式来实现多数据源事务中正确使用对应数据源的方法,即使用SqlSession

3.1 SqlSession管理类

import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class MultiSqlSessionManager {
    @Autowired
    @Qualifier("ds1SqlSessionTemplate")
    private SqlSession sqlSession1;  // 对应dataSource1

    @Autowired
    @Qualifier("ds2SqlSessionTemplate")
    private SqlSession sqlSession2;  // 对应dataSource2

    public SqlSession getSqlSession(String dataSourceKey) {
        if ("dataSource1".equals(dataSourceKey)) {
            return sqlSession1;
        } else if ("dataSource2".equals(dataSourceKey)) {
            return sqlSession2;
        }
        return sqlSession1;
    }
}

3.2 改造Mapper切面

注入SqlSession管理类,并新增了是否启用多数据源事务的判断。

    @Autowired
    private MultiSqlSessionManager multiSqlSessionManager;
    
    @Around("mapperInterface1() || mapperInterface2()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String mapperClassName = method.getDeclaringClass().getName();
        DataSourceType currentMapper = DynamicDataSourceContextHolder.getCurrentMapperType();
        // 根据Mapper接口的全限定名获取对应的数据源名称,动态配置中获取,Nacos或drm
        String dataSourceName = mapps.get(mapperClassName);
        DataSourceType dataSourceType = DataSourceType.getByValue(dataSourceName);
        log.info("当前使用的数据源,currentMapperDataSource={},toMapperDataSource:{},mapperClassName:{}#{}", currentMapper,
                Objects.isNull(dataSourceType) ? "null" : dataSourceType.getValue(), mapperClassName, method.getName());
        
        // 是否启用了多数据源事务
        Boolean enableMultiTransaction = TransactionManageAspect.getEnableMultiTransaction();
        if (Objects.nonNull(enableMultiTransaction) && enableMultiTransaction) {

            // 获取对应的SqlSession
            SqlSession targetSqlSession = multiSqlSessionManager.getSqlSession(dataSourceType.getValue());
            // 获取Mapper接口
            Class<?> mapperInterface = method.getDeclaringClass();
            // 从目标SqlSession中获取Mapper实例
            Object targetMapper = targetSqlSession.getMapper(mapperInterface);

            // 执行方法
            Object[] args = joinPoint.getArgs();

            return method.invoke(targetMapper, args);
        }
        try {
            DynamicDataSourceContextHolder.setCurrentMapperType(dataSourceType);
            return joinPoint.proceed();
        } finally {
            DynamicDataSourceContextHolder.clearCurrentMapperType();
        }
    }

3.3 改造多数据源事务切面

本切面在上一篇文章已经介绍过,下面改造后的全部代码。新增了线程变量ENABLE_MULTI_DS和获取其值的方法:getEnableMultiTransaction。

package com.example.demo.common.dal.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import java.util.stream.Collectors;

@Slf4j
@Aspect
@Component
@Order(0)
public class TransactionManageAspect {

    @Resource
    private ApplicationContext applicationContext;

    private static final ThreadLocal<Boolean> ENABLE_MULTI_DS = new ThreadLocal<>();

    public static Boolean getEnableMultiTransaction() {
        return ENABLE_MULTI_DS.get();
    }

    /**
     * 切点
     */
    @Pointcut("@annotation(com.example.demo.common.dal.aspect.MultiTransactionManager)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法上的注解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        log.info("事务管理器切面start, methodName={}", method.getName());
        MultiTransactionManager multiTransactionManager = joinPoint.getTarget().getClass().getAnnotation(MultiTransactionManager.class);
        if (multiTransactionManager == null) {
            multiTransactionManager = method.getAnnotation(MultiTransactionManager.class);
        }
        // 事务管理器
        Stack<DataSourceTransactionManager> transactionManagerStack = new Stack<>();
        // 事务状态
        Stack<TransactionStatus> transactionStatusStack = new Stack<>();
        boolean isTransaction = true;
        if (multiTransactionManager.transactionManagerNames().length < 1) {
            log.info("事务管理器名称数组为空, methodName={}", method.getName());
            isTransaction = false;
        }
        if (isTransaction) {
            // 去重
            List<String> collect = Arrays.asList(multiTransactionManager.transactionManagerNames()).stream()
                    .distinct()
                    .collect(Collectors.toList());
            for (String transactionManagerName : collect) {
                DataSourceTransactionManager dataSourceTransactionManager = applicationContext.getBean(transactionManagerName,
                        DataSourceTransactionManager.class);
                if (dataSourceTransactionManager == null) {
                    log.warn("事务管理器不存在,transactionManagerName:{},methodName={}", transactionManagerName, method.getName());
                    continue;
                }
                TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(new DefaultTransactionDefinition());
                transactionManagerStack.push(dataSourceTransactionManager);
                transactionStatusStack.push(transactionStatus);
            }
        }
        if (CollectionUtils.isEmpty(transactionManagerStack)) {
            log.info("事务管理器为空, methodName={}", method.getName());
            isTransaction = false;
        }
        try {
            if (isTransaction) {
                ENABLE_MULTI_DS.set(true);
            }
            Object obj = joinPoint.proceed();
            // 未发生异常,提交事务
            while (isTransaction && !transactionManagerStack.isEmpty()) {
                DataSourceTransactionManager dataSourceTransactionManager = transactionManagerStack.pop();
                TransactionStatus transactionStatus = transactionStatusStack.pop();
                dataSourceTransactionManager.commit(transactionStatus);
            }
            log.info("事务管理器切面提交, methodName={}", method.getName());
            return obj;
        } catch (Throwable e) {
            log.error("事务执行异常, methodName={}", method.getName(), e);
            while (isTransaction && !transactionManagerStack.isEmpty()) {
                DataSourceTransactionManager dataSourceTransactionManager = transactionManagerStack.pop();
                TransactionStatus transactionStatus = transactionStatusStack.pop();
                dataSourceTransactionManager.rollback(transactionStatus);
            }
            log.info("事务管理器切面回滚, methodName={}", method.getName());
            throw e;
        } finally {
            log.info("事务管理器切面end, methodName={}", method.getName());
        }
    }
}

4.总结

  1. 在无事务的业务方法中,可以mapper层切面中可以随时切换数据源。(因为每次都会获取新的连接。)
  2. 在有事务的业务方法中,使用SqlSession可以使之正确的处理每个Mapper层接口的方法的执行。

多数据源事务提交

本文展示的代码均以Spring-AOP为基础,使用切面的方式进行多数据源事务提交。

1. 数据源配置

定义了2个数据源和2个事务管理器。

<!-- 定义数据源 -->
<bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>

<bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url2}" />
    <property name="username" value="${jdbc.username2}" />
    <property name="password" value="${jdbc.password2}" />
</bean>

<!-- 定义事务管理器 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<bean id="txManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource2"/>
</bean>

2. 编写切面相关代码

2.1 自定义注解

此注解用于方法上面,进行事务关联。

import java.lang.annotation.ElemntType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
public @interface MultiTxManager {
    /**
     * 事务管理器名称
     */
    String[] txManagerNames() default {};
}

2.2 切面代码

用于事务提交和回滚。事务管理器的2大作用:提交和回滚。


import org.apache.commons.collections4.CollectionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Stack;

@Aspect
@Component
public class SelfTxManagerAspect {

    @Resource
    private ApplicationContext context;

    /**
     * 定义切点
     */
    @Pointcut("@annotation(MultiTxManager)")
    public void pointCut() {
    }

    /**
     * 环绕
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        MultiTxManager tm = method.getAnnotation(MultiTxManager.class);

        // 事务管理器
        Stack<DataSourceTransactionManager> tmStack = new Stack<>();
        // 事务状态
        Stack<TransactionStatus> tsStack = new Stack<>();

        // 是否需要开启事务标识
        boolean enableTransaction = true;
        if (tm.txManagerNames().length < 1) {
            enableTransaction = false;
        }

        if (enableTransaction ) {
            for(String tmName : tm.txManagerNames()) {
                DataSourceTransactionManager dtxm = context.getBean(tmName,DataSourceTransactionManager.class);
                if (dtxm == null) { continue; }
                // 开启事务
                TransactionStatus ts = dtxm.getTransaction(new DefaultTransactionDefinition());
                tmStack.push(dtxm);
                tsStack.push(ts);
            }
        }
        if (CollectionUtils.isEmpty(tmStack)) {
            enableTransaction = false;
        }
        try {
            Object result = point.proceed();
            // 方法执行完成,若有事务则提交
            while (enableTransaction && !tmStack.isEmpty()) {
                DataSourceTransactionManager manager = tmStack.pop();
                TransactionStatus status = tsStack.pop();
                manager.commit(status);
            }
            return result;
        } catch (Throwable e) {
            // 发生异常时回滚
            while (enableTransaction && !tmStack.isEmpty()) {
                DataSourceTransactionManager manager = tmStack.pop();
                TransactionStatus status = tsStack.pop();
                manager.rollback(status);
            }
            throw e;
        }
    }
}

2.3 在方法上应用

@Service
public class xxxxServiceImpl {

    @MultiTxManager(txManagerNames={"txManager", "txManager2"})
    public Object saveOrUpdate() {
        // 做一些数据库更新操作
    }
}

如有问题请留言。

Mockito使用问题合集

1.mock泛型方法

  • 反射获取Method时,泛型类型使用 Object.class 替换。
  • 调用方法时,泛型入参,不能使用new Object(), 需要使用具体的实现类。

1.1 源代码

public abstract class Test<OUT> {
    
    private void setOutPut(OUT model, Map<String, String> params) {
            // do something
    }
}

public class TestSub extends Test<TestSubDO>{
    // do something
}

1.2 mock代码

@RunWith(MockitoJUnitRunner.class)
public class TestTest {
    @InjectMock
    private Test test = new TestSub();
    
    @Test
    public void test1(){
        // OUT 使用 Object.class 替换
        Method method = Test.class.getDeclaredMethod("setOutPut", Object.class, Map.class);
        method.setAccessible(true);
        // TestSubDO
        method.invoke(test, new TestSubDO(), new HashMap());
    }
}

2.循环中多次调用同一方法

在一个循环中多次调用同一个有返回值的方法,模拟多次返回值,需要多次使用thenReturn方法,或在thenReturn中设置多个值。

2.1源代码

while(true){
    Page<TestPO> pageList = testMapper.selectPage(pageQuery);
    if(CollectionUtils.isEmpty(pageList.getData())) {
        break;    
    }
    // todo something 
}

2.2 mock代码

Page<TestPO> page1=new Page(1,10,1,Lists.newArrayList(new TestPO()));
Page<TestPO> page2=new Page(1,10,1,Collections.emptyList());
// 第二次调用返回数据为空,则会退出while循环
when(testMapper.selectPage(any())).thenReturn(page1).thenReturn(page2);

3.MockitoException for Static Mocks异常

在test/resources文件夹下追加:

  1. 新建文件夹 mockito-extensions
  2. 在新的文件夹中新建文本文件 org.mockito.plugins.MockMaker
  3. 向文本中写入内容:mock-maker-inline

4.模拟protected方法

有2种方式可以模拟protected方法。

4.1方式1使用ReflectionSupport

ReflectionSupport类是JUnit5提供的一个类。可以使用 invokeMethod方法,执行protected方法。

public static Object 
invokeMethod(Method method, Object target, Object ...args)

4.2方式2使用Answers.CALLS_REAL_METHODS

注意:这种方式时,需要将xxxTest.java的package路径和xxx.java类的package路径保持一致,不一致时调用protected方法时报错。

Abstarct ab = mock(Abstarct.class, Answers.CALLS_REAL_METHODS);
ab.protectedMethod();

5.mybatis-plus相关问题

5.1 MyBatisPlusException:can not find lambda cache for this entity

解决方法是在单测方法第一行加入一行代码:

TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "")
, TestPO.class);

注意:TestPO是数据库表对应的实体类。

6.模拟EasyExcel读取数据

6.1源代码

读取Excel文件时使用以下方法:

EasyExcel.read(String filePath, Class head, ReadListener listener);

源代码应用:

@Data
public class TestDO {
    @ExcelProperty(value = "名称", index = 0)
    private String name;
}

public class ReadTestListener extend AnalysisEventListener<TestDO> {

    private List<TestDO> dataList = new ArrayList<>();
    public List<TestDO> getDataList() {
        return this.dataList;
    }

    // 覆写方法 invoke, doAfterAllAnalysed 
}

// 后面要mock的方法
public void readExcel(String filePath) {
    ReadTestListener listener = new ReadTestListener();
    EasyExcel.read(filePath, TestDO.class, listener).sheet().doRead();
}

6.2 mock代码

@Test
public void test1() {
    String filepath = "c:\\testdata\testDo.xlsx";
    ExcelReaderBuilder readerBuilder = Mockito.mock(ExcelReaderBuilder.class);
    try(MockedStatic mockEasyExcel = Mockito.mockStatic(EasyExcelFactory.class, invocation -> {
        if (invocation.getMethod().getName().equals("read")) {
            return readerBuilder;        
        }    
        return null;
    })) {
            ExcelReaderSheetBuilder excelReaderSheetBuilder=mock(ExcelReaderSheetBuilder.class);
            when(readerBuilder.sheet()).thenReturn(excelReaderSheetBuilder);
            doNothing().when(excelReaderSheetBuilder).doRead();
            
            // 调用mock的方法
            readExcel(filepath);
    }
}

7.模拟Class对象

2种方式:

// 方式1
Mockito.doReturn(UserInfo.class).when(method).getDeclaringClass();

// 方式2
Mockito.<Class<?>>when(method.getDeclaringClass())
                      .thenReturn(UserInfo.class);

后续遇到问题会继续补充。

EasyExcel识别表头

本文将介绍使用EasyExcel实现识别单行表头的一种实现方式。主要是通过实现AnalysisEventListener 抽象类,并覆写其 hasNext 和 invokeHead 方法。

Jar依赖

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>easyexcel</artifactId>
	<version>3.3.4</version>
</dependency>

<dependency>
	<groupId>org.reflections</groupId>
	<artifactId>reflections</artifactId>
	<version>0.10.2</version>
</dependency>

实现代码

一个表头基类和2个子类,一个用于表头字段的类。

注意:代码使用反射,使用了@ExcelProperty(value = “地址”, index = 0)注解,index用于表头字段排序,value用于表头显示的名称。

1.表头基类
package top.jyokiyi.my.test.excel;
import lombok.Data;

@Data
public class ExcelBaseModel {
}
2.表头实现类1

继承表头基类:

package top.jyokiyi.my.test.excel;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

@Data
public class ExcelHeadTest1Model extends ExcelBaseModel {

    @ExcelProperty(value = "地址", index = 0)
    private String address;

    @ExcelProperty(value = "名称", index = 1)
    private String name;
}
3.表头实现类2

继承表头基类:

package top.jyokiyi.my.test.excel;

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

@Data
public class ExcelHeadTest2Model extends ExcelBaseModel {

    @ExcelProperty(value = "邮件",index = 0)
    private String email;

    @ExcelProperty(value = "内容", index = 1)
    private String content;
}
4. 表头字段类
package top.jyokiyi.my.test.excel;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ExcelHeadFieldModel {
    /**
     * 列号
     */
    private Integer column;
    /**
     * 表头描述
     */
    private String headDesc;
}
5. 使用Reflections实现查找所有子类

注意:这里直接指定了包名,且子类和基类在同一个包中。(如有需要可自行修改,以实现自己的需求)


/**
 * 获取指定类的所有子类
 * @param clazz
 * @param <T>
 * @return
 */
public static <T>Set<Class<? extends T>> getAllSubTypes(Class<T> clazz) {
	Set<Class<? extends T>> subTypeClassSet = new HashSet<>();

	Reflections reflections = new Reflections("top.jyokiyi.my.test.excel");
	Set<Class<? extends T>> subTypesOf = reflections.getSubTypesOf(clazz);
	if (CollectionUtils.isNotEmpty(subTypesOf)) {
		subTypeClassSet.addAll(subTypesOf);
	}

	return subTypeClassSet;
}
6.表头类型与表头字段集合映射
/**
 * 将表头与对于的类映射起来
 * @param sets
 * @param <T>
 * @return
 */
public static <T>Map<Class<? extends T>, List<ExcelHeadFieldModel>> getHeadMapForExcelProperty(Set<Class<? extends T>> sets) {
	Map<Class<? extends T>, List<ExcelHeadFieldModel>> maps = new HashMap<>();

	for (Class<? extends T> clz : sets) {
		List<ExcelHeadFieldModel> fieldModelList = new ArrayList<>();
		try {
			Field[] fields = clz.getDeclaredFields();
			for (Field field : fields) {
				if (field.isAnnotationPresent(ExcelProperty.class)) {
					ExcelProperty annotation = field.getAnnotation(ExcelProperty.class);
					fieldModelList.add(ExcelHeadFieldModel.builder()
							.column(annotation.index()).headDesc(annotation.value()[0]).build());
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (CollectionUtils.isNotEmpty(fieldModelList)) {
			maps.put(clz, fieldModelList.stream()
                       .sorted(Comparator.comparing(ExcelHeadFieldModel::getColumn))
                       .collect(Collectors.toList()));
		}
	}
	return maps;
}
7.实现监听类

继承easy excel 的 AnalysisEventListener 抽象类,并覆写其 hasNext 和 invokeHead 方法。

package top.jyokiyi.my.test.excel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.util.StringUtils;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

public class ExcelHeaderIdentifyListener extends AnalysisEventListener<Map<Integer, String>> {

    private static ThreadLocal<Class<? extends ExcelBaseModel>> classResult = new ThreadLocal<>();

    public ThreadLocal<Class<? extends ExcelBaseModel>> getClassResult() {
        return classResult;
    }

    public void clear() {
        classResult.remove();
    }

    /**
     * 返回false,读取一行后,不再继续读取
     *
     * @param context
     * @return
     */
    @Override
    public boolean hasNext(AnalysisContext context) {
        return false;
    }

    /**
     * 根据表头解析获取对应的表头子类
     * @param headMap
     * @param context
     */
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        List<ExcelHeadFieldModel> headFieldModels = headMap.values().stream().map(column -> ExcelHeadFieldModel.builder()
                .column(column.getColumnIndex()).headDesc(column.getStringValue()).build())
                .sorted(Comparator.comparing(ExcelHeadFieldModel::getColumn))
                .collect(Collectors.toList());
        // 将表头字段拼接起来
        String joinStr = headFieldModels.stream()
                .map(ExcelHeadFieldModel::getHeadDesc)
                .collect(Collectors.joining());

        // 获取表头基类的所有子类信息
        Set<Class<? extends ExcelBaseModel>> allSubTypes = ClassUtils.getAllSubTypes(ExcelBaseModel.class);
        // 将class和表头映射起来
        Map<Class<? extends ExcelBaseModel>, List<ExcelHeadFieldModel>> listMap = ClassUtils.getHeadMapForExcelProperty(allSubTypes);
        // 查找对应的子类
        for (Map.Entry<Class<? extends ExcelBaseModel>, List<ExcelHeadFieldModel>> entry : listMap.entrySet()) {
            if (entry.getValue().size() == headFieldModels.size() && StringUtils.equals(joinStr, entry.getValue()
                    .stream().map(ExcelHeadFieldModel::getHeadDesc).collect(Collectors.joining()))) {
                classResult.set(entry.getKey());
                break;
            }
        }
    }
    

    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {

    }


    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {

    }
}
8.测试
package top.jyokiyi.my.test.excel;

import com.alibaba.excel.EasyExcel;

import java.io.File;
import java.io.FileInputStream;

public class ReadTest {


    public static void main(String[] args) throws Exception {
        ExcelHeaderIdentifyListener listener = new ExcelHeaderIdentifyListener();
        String dir = System.getProperty("user.dir");
        String filePath = dir + File.separator + "src" + File.separator + "test_excel.xlsx";

        EasyExcel.read(new FileInputStream(filePath), listener)
                 .sheet().doRead();

        Class<? extends ExcelBaseModel> aClass = listener.getClassResult();
        listener.clear();
        System.out.println(aClass.getName());
    }
}

测试结果:

top.jyokiyi.my.test.excel.ExcelHeadTest1Model

测试文件的表头如下:

地址名称
12

x86-64汇编之Hello world

1. 环境

WSL子系统:Ubuntu

2. 工具

所需工具包列表:

程序名称检查命令所需的包
GUN Assembleras –versionbinutils
GUN Linkerld –versionbinutils
GUN Debuggergdb –versiongdb
Makemake –versionmake

3. 代码

代码使用语法风格是:AT&T。

.global _start
.section .data
mystr:
    .string "Hello, world!\n"
mystrend:
.section .text
_start:
    # 设置系统调用号,1 表示write
    movq $1, %rax
    # 标准输出stdout
    movq $1, %rdi
    # 字符串指针
    leaq mystr(%rip), %rsi
    # 字符串长度
    movq $(mystrend-mystr), %rdx
    # 执行系统调用
    syscall

    # 设置系统调用号,60 表示exit
    movq $60, %rax
    syscall

4.编译链接并执行

# 编译
as helloworld.s -o helloworld.o
# 链接
ld helloworld.o -o helloworld
# 执行,控制台会输出 :Hello, world!
./helloworld

参考资料:

  1. x64架构体系的系统调用列表:https://x64.syscall.sh/

No CMAKE_CXX_COMPILER could be found.

错误信息

在使用cmake 编译 flex&bison程序时,使用cmake的project()指令,编译报以下错误,错误具体信息:

CMake Error at CMakeLists.txt:3 (project):
  No CMAKE_CXX_COMPILER could be found.

  Tell CMake where to find the compiler by setting either the environment
  variable "CXX" or the CMake cache entry CMAKE_CXX_COMPILER to the full path
  to the compiler, or to the compiler name if it is in the PATH.


-- Configuring incomplete, errors occurred!

原因是:cmake 的project() 命令在没有指定 LANGUAGES 选项时,默认是C和CXX(注意cmake 3.0之前的版本不支持LANGUAGES 关键字),CXX编译时需要指定 g++编译器。

解决方法

有2种方式:

方式一

在project() 命令中指定LANGUAGES 选项为C:

project(test1 LANGUAGES C)

方式二

安装g++ :

apt install g++

flex的开始状态

是在flex定义部分中声明,开始状态(start states),也称为 开始条件(start conditions) 或者 开始规则(start rules)。

开始状态的作用

是用来限制特定规则的作用范围,或者针对文件的部分内容来改变扫描器的工作方式。即根据开始状态激活对应的规则(rules)。

开始状态的模式

开始状态有2种模式:

  1. %s :inclusive (包含模式),它允许未标记任何开始状态的rule 可以进行匹配。
  2. %x:exclusive(独占模式),它只允许标记为该开始状态下的rule 进行匹配。通常独占模式更有用。

注意:在 flex定义部分中声明时 使用非缩进的形式。例如:

/* definitions 定义部分 */
%{

%}
%s simple
%x test
%%
/*rule action  规则和执行动作 部分*/
<simple>[0-9]+ {}
<test>[a-z]+ {}
%%
/* helper function 辅助函数部分*/

注意在规则部分中:开始状态名称和rule之间是没有空格的

开始状态的激活

开始状态的激活需要使用:宏 BEGIN 进行激活。BEGIN的语法:

BEGIN  statename;

statename 是使用 %x 或 %s 声明的开始状态名称。Flex默认的开始状态是 零 状态,也称为 INITIAL。BEGIN(0)等价于 BEGIN(INITIAL)。

注意:宏 BEGIN 本身是没有任何参数的,状态名字也不应该被括号括起来,但是加括号是一种良好的风格。

示例

分别使用2种状态模式对字符串数字”123.45″进行识别:

%{
    #include <stdio.h>
    #include <math.h>
%}

%s expect

%%

"expect-floats" BEGIN(expect);

<expect>[0-9]+"."[0-9]+ {
    printf("found a float,=%f\n", atof(yytext));
}

<expect>\n {
    /* BEGIN(INITIAL); */
}

[0-9]+ {
    printf("found an integer, = %d\n", atoi(yytext));
}

"." {
    printf("found a dot \n");
}

%%

int main()
{
    yylex();
    return 1;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)

project(startConditionTest LANGUAGES C)

find_package(FLEX)

FLEX_TARGET(myscanner startcondition_test.l ${CMAKE_CURRENT_BINARY_DIR}/startConditionTest.lex.c)

# 设置主要包含的c文件
set(MAIN_SRC ${CMAKE_CURRENT_BINARY_DIR}/startConditionTest.lex.c)

# 使用源文件生成可以执行文件
add_executable(conditionTest01 ${MAIN_SRC})

# libfl.a
find_library(LEX_LIB fl)
TARGET_LINK_LIBRARIES(conditionTest01 ${LEX_LIB})

包含模式%s

使用包含模式:%s expect.

编译并执行:

mkdir build
cd build
cmake ../
make
# 执行
./conditionTest01 
123.45
found an integer, = 123
found a dot 
found an integer, = 45

"expect-floats" 123.45
"" found a float,=123.450000
123
found an integer, = 123

执行结果如下图所示:

独占模式%x

使用独占模式:%x expect

编译执行:

└─# ./conditionTest01 
123.456
found an integer, = 123
found a dot 
found an integer, = 456

"expect-floats"
""123.45
found a float,=123.450000
123456
123456

789
789

如下图所示:

总结

从执行结果上面可以看到:

  1. 独占模式被激活时,没有标记任何开始状态的rule,将不会被匹配。
  2. 包含模式被激活后,未标记开始状态的rule 也是可以进行匹配的。
  3. 和开始状态的模式小节中描述的是一致的。

使用cmake编译flex

环境

Linux系统

cmake版本:cmake version 3.25.1

make版本:GNU Make 4.3

flex版本:flex 2.6.4

flex源文件

catcot.l 文件

%{
    #include <stdio.h>    
%}

%%
c.t { printf("mumble mumble"); }
cot { printf("portable bed"); }
cat { printf("thankless pet"); }
cats { printf("anti-herd");}

%%

int main()
{
    /* 调用获取token */
    yylex();
    return 0;
}

CMakeLists.txt文件

# 指定cmake 最低版本号
cmake_minimum_required(VERSION 3.20)

# 指定项目名称 和 语言
project(catcot1 LANGUAGES C)

# 查找依赖包
find_package(FLEX)

# 使用宏定义生成规则
FLEX_TARGET(catcotScanner catcot.l ${CMAKE_CURRENT_BINARY_DIR}/catcot.lex.c)

# 设置主要包含的c文件
set(MAIN_SRC ${CMAKE_CURRENT_BINARY_DIR}/catcot.lex.c)

# 使用源文件生成可以执行文件
add_executable(catcotTest ${MAIN_SRC})

编译

# 第一步
mkdir build
# 第二步
cd build
# 第三步
cmake ../
# 第四步
make 

问题

在编译部分 第四步 时会出现如下的错误提示:

/usr/bin/ld: CMakeFiles/catcotTest.dir/catcot.lex.c.o: in function `yylex':
catcot.lex.c:(.text+0x4e0): undefined reference to `yywrap'
/usr/bin/ld: CMakeFiles/catcotTest.dir/catcot.lex.c.o: in function `input':
catcot.lex.c:(.text+0x10de): undefined reference to `yywrap'
collect2: error: ld returned 1 exit status
make[2]: *** [CMakeFiles/catcotTest.dir/build.make:101: catcotTest] Error 1
make[1]: *** [CMakeFiles/Makefile2:83: CMakeFiles/catcotTest.dir/all] Error 2
make: *** [Makefile:91: all] Error 2

问题解决

解决上面的undefined reference to `yywrap’问题,有以下三种方法:

  • 方法1:在 catcot.l 文件头部添加:%option noyywrap
  • 方法2:在 catcot.l 文件中添加自己的yywrap()函数,返回值1
int yywrap()
{
   return 1;
}
  • 方法3:在CMakeLists.txt文件末尾追加:
# 添加 libfl.a 库
find_library(LEX_LIB fl)
# 链接 libfl.a 库
TARGET_LINK_LIBRARIES(catcotTest ${LEX_LIB})

这样可以使用默认flex的库libfl.a 生成的默认yywrap函数。

以上3种方法,均已验证是可以的。

推荐使用第 1 种方法

使用Flex&Bison时可能的问题和解决方法

问题1:cannot find ll

意思是找不到 libl.a库文件。

问题2:cannot find fl

意思是找不到 libfl.a库文件。

问题3:cannot find ly

意思是找不到 liby.a库文件。

解决方法

下面是在Ubuntu系统上的解决方法介绍:

  1. 安装apt-file:apt install apt-file
  2. 使用apt-file命令查找库在哪个工具包中:
apt-file search libl.a
apt-file search libfl.a
apt-file search liby.a

查找结果:

3. 安装对应的工具包就可以了

apt install libfl-dev libbison-dev

apt-cache 是使用包名或包描述 检索仓库。

apt-file 是 使用包中文件名检索存储库(工具包名)。

Linux centos 对应的是 yum whatprovides ,具体使用方法请自行查阅。

语法分析器Bison安装

Bison源码地址:https://github.com/akimd/bison

Linux环境安装

方式一

到Bison官网上给定的下载地址:https://ftp.gnu.org/gnu/bison/

下载完成后,解压后进入文件夹目录按照INSTALL文件内容中的安装步骤即可。

  • 执行配置:./configure ,执行后可能会检查出来缺少依赖,按照提示的内容,安装缺少的依赖就可以了。
  • 执行编译和检查:make && make check
  • 执行安装:sudo make install ,默认是安装在 /usr/local/bin目录下。
  • 安装成功后,输入bison --version ,输出版本号就表示成功了。

方式二

直接命令安装:sudo apt install bison

Windows环境安装

在上一篇文章中,Flex安装中已经说明安装了MinGW。下面直接说安装Bison。

选择最新的版本下载,并选择安装到和Flex一个目录中。之前flex的bin目录已经添加到了系统环境path中了。这里直接打开cmd控制台,输入:bison --version 输出正确的版本即成功安装。