SpringBoot3(若依框架)集成Mybatis-Plus和单元测试功能,以及问题解决
一、Mybatis-Plus集成
- 新增依赖到父级pom.xml,原先的mybatis依赖可以不动
需要注意 mybatis-plus与mybatis版本之间的冲突,不要轻易改动依赖,不然分页也容易出现问题
分类顶级pom.xml下面,如果没有引入还是出现报错,在common的模块下面再引入一份下面的依赖 <!-- springboot3 / mybatis-plus 配置 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.10</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> <version>3.5.10</version> </dependency>
- 替换原来的 MyBatis 配置,修改application.yml文件,修改mybatis配置为mybatis-plus
# MyBatis Plus配置mybatis-plus: # 搜索指定包别名 typeAliasesPackage: com.ruoyi.**.domain # 配置mapper的扫描,找到所有的mapper.xml映射文件 mapperLocations: classpath*:mapper/**/*Mapper.xml # 加载全局的配置文件 configLocation: classpath:mybatis/mybatis-config.xml
- 删除或者修改MyBatisConfig.java 配置 ,新增MybatisPlusConfig配置
@EnableTransactionManagement(proxyTargetClass = true)@Configurationpublic class MybatisPlusConfig { public static final ThreadLocal<String> TABLE_NAME = new ThreadLocal<>(); @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 1. 动态表名配置 (兼容 MyBatis-Plus 3.5.5) DynamicTableNameInnerInterceptor dynamicTableNameInterceptor = new DynamicTableNameInnerInterceptor(); // 创建表名处理器映射 (3.5.5 版本使用 setTableNameHandler 方法) Map<String, TableNameHandler> tableNameHandlerMap = new HashMap<>(); BaseTableNameEnum.getAllBaseTableName().forEach(table -> tableNameHandlerMap.put(table, (sql, oldTable) -> Optional.ofNullable(TABLE_NAME.get()).orElse(oldTable) )); // 3.5.5 版本设置表名处理器的方式 dynamicTableNameInterceptor.setTableNameHandler( (sql, tableName) -> { TableNameHandler handler = tableNameHandlerMap.get(tableName); return handler != null ? handler.dynamicTableName(sql, tableName) : tableName; } ); interceptor.addInnerInterceptor(dynamicTableNameInterceptor); // 2. 分页插件配置 interceptor.addInnerInterceptor(paginationInnerInterceptor()); // 3. 添加自定义的动态创建表拦截器 interceptor.addInnerInterceptor(new DynamicCreateTableInterceptor()); // 4. 攻击 SQL 阻断插件 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } /** * 分页插件,自动识别数据库类型 */ public PaginationInnerInterceptor paginationInnerInterceptor() { PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); // 设置数据库类型为mysql paginationInnerInterceptor.setDbType(DbType.MYSQL); // 设置最大单页限制数量,默认 500 条,-1 不受限制 paginationInnerInterceptor.setMaxLimit(-1L); return paginationInnerInterceptor; } /** * 乐观锁插件 */ public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() { return new OptimisticLockerInnerInterceptor(); } /** * 如果是对全表的删除或更新操作,就会终止该操作 */ public BlockAttackInnerInterceptor blockAttackInnerInterceptor() { return new BlockAttackInnerInterceptor(); }}
可以不用参考我的,因为我新增了拦截分表功能,官网有配置参考,可以参考官网
https://doc.ruoyi.vip/ruoyi/document/cjjc.html#%E9%9B%86%E6%88%90mybatis-plus%E5%AE%9E%E7%8E%B0mybatis%E5%A2%9E%E5%BC%BA
- 仅供参考的分表功能 MybatisPlusUtils 和 DynamicCreateTableInterceptor
/** * MybatisPlus工具类 * */public class MybatisPlusUtils { /** * 获取动态表名 * * @return */ public static String getDynamicTableName() { return MybatisPlusConfig.TABLE_NAME.get(); } /** * 设置动态表名 */ public static void setDynamicTableName(String tableName) { MybatisPlusConfig.TABLE_NAME.set(tableName); } /** * 清空当前线程设置的动态表名 * * @return * @author wk * @date 2022/2/9 10:37 */ public static void emptyDynamicTableName() { MybatisPlusConfig.TABLE_NAME.remove(); } /** * 获取基础表 * * @return */ public static String getBaseTableName(String dynamicTableName) { return BaseTableNameEnum.getBaseTableName(dynamicTableName); }}
/** * 动态创建表拦截器 * */@Slf4jpublic class DynamicCreateTableInterceptor implements InnerInterceptor { /** * 动态创建表 * * @param sh * @param connection * @param transactionTimeout */ @Override public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { String dynamicTableName = MybatisPlusUtils.getDynamicTableName(); if (StringUtils.isBlank(dynamicTableName)) { return; } String baseTableName = MybatisPlusUtils.getBaseTableName(dynamicTableName); if (StringUtils.isNotBlank(baseTableName)&&!baseTableName.contains(\"null\")) { try { String dataBase = connection.getCatalog(); String sql = \"SELECT count(1) FROM information_schema.tables WHERE table_schema=? AND table_name = ?\"; PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, dataBase); preparedStatement.setString(2, dynamicTableName); ResultSet resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { //获取表是否存在 int count = resultSet.getInt(1); close(preparedStatement, resultSet); //如果表不存在 if (count == 0) { sql = \"SHOW CREATE TABLE \" + baseTableName; //获取创建表语句 preparedStatement = connection.prepareStatement(sql); resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { String createTableSql = resultSet.getString(2); close(preparedStatement, resultSet); //创建表 sql = createTableSql.replaceFirst(baseTableName, dynamicTableName); preparedStatement = connection.prepareStatement(sql); preparedStatement.executeUpdate(); close(preparedStatement, resultSet); log.info(\"【动态创建表成功】表名:{}\", dynamicTableName); } else { close(preparedStatement, resultSet); } } } else { close(preparedStatement, resultSet); } } catch (Exception e) { log.info(String.format(\"【动态创建表失败】表名: %s\", dynamicTableName), e); } } } /** * 关闭资源 * * @param preparedStatement * @param resultSet */ private void close(PreparedStatement preparedStatement, ResultSet resultSet) { if (preparedStatement != null) { try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if (resultSet != null) { try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } }}
- 使用方式在需要增删改查的地方调用方法即可
/** * 添加任务(使用动态表名) */ @Transactional @Override public void addTaskWithDynamicTable(实体类 task) { task.setCreateTime(DateUtils.getNowDate()); try { // 设置动态表名(按月份分表) String monthSuffix = LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"yyyy\")); String dynamicTable = \"表名_\" + monthSuffix; MybatisPlusUtils.setDynamicTableName(dynamicTable); // 插入数据(会自动使用动态表名) int result = 当前实现类的方法.insert(task); log.info(\"【动态表名插入】表名: {}, 结果: {}\", dynamicTable, result); } finally { MybatisPlusUtils.emptyDynamicTableName(); } }
二、集成单元测试
- 使用依赖,在ruoyi-admin的pom.xml下添加依赖
<!-- 单元测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
不需要指定版本,自动依赖,下面是错误案例,在其他模块引入了单元测试版本不一样,造成混乱
<!-- 单元测试--><!-- <dependency>--><!-- <groupId>org.springframework.boot</groupId>--><!-- <artifactId>spring-boot-test</artifactId>--><!-- <scope>test</scope>--><!-- </dependency>--><!-- <dependency>--><!-- <groupId>org.junit.jupiter</groupId>--><!-- <artifactId>junit-jupiter</artifactId>--><!-- <scope>test</scope>--><!-- </dependency>--><!-- <dependency>--><!-- <groupId>org.springframework</groupId>--><!-- <artifactId>spring-test</artifactId>--><!-- <scope>test</scope>--><!-- </dependency>--><!-- <dependency>--><!-- <groupId>junit</groupId>--><!-- <artifactId>junit</artifactId>--><!-- <version>4.13.2</version>--><!-- </dependency>-->
- 简简单单才是最好的,不然容易出现错误,一直以为引入的是JUnit 5结果是JUnit 4导致运行失败
,在ruoyi-admin的src下创建test模块(与main同级),导入依赖后更新maven,确保依赖加入
/** 1. @description 测试MybatisPlus和分表功能 *///JUnit 4//@SpringBootTest(classes = Application.class,// webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)//@RunWith(SpringRunner.class)//JUnit 5(\"MybatisPlus测试\")public class MybatisPlusTest { //测试分页功能 private service方法 taskMapper; private ISysUserService sysUserService; private RuoYiConfig ruoYiConfig; public void testContextLoad() { assertNotNull(\"taskService 注入失败\", taskMapper); assertNotNull(\"sysUserService 注入失败\", sysUserService); log.info(\"服务注入测试通过:\"+ruoYiConfig.getName()); } /** * 添加任务(使用动态表名) */ (\"添加任务(使用动态表名)\") public void addTaskWithDynamicTable() { try { 实体类 task = new 实体类(); task.settName(\"张三\"); task.setAge(\"18\"); task.setPatientSex(1); task.setMobile(\"13888888888\"); task.setIdCard(\"420000000000000000\"); // 插入数据(会自动使用动态表名) taskMapper.insertHosCollectTaskInfo(task); } finally {// MybatisPlusUtils.emptyDynamicTableName(); } } (\"测试查询功能\") public void testContextLoads() { log.info(\"Spring上下文加载成功,taskMapper已注入\"); } (\"测试查询功能\") public void testSelect() { log.info(\"测试查询功能\"); SysUser sysUser = sysUserService.selectUserById(1L); log.info(\"查询结果:{}\", sysUser); }}
三、问题解决
- 在 JUnit 5 中,不需要 @RunWith(SpringRunner.class),直接使用 @SpringBootTest 即可
- 在 JUnit 4 中,通常需要显式指定 @RunWith(SpringRunner.class),使用RunWith才能实例化到spring容器中
- JUnit 4如何没有引入 @RunWith,就会出现 NullPointerExecption,是因为 Spring 的依赖注入没有正确完成,或者相关的 Bean 没有被正确加载。
- 如果添加完成还是没有完成,还是服务注入失败问题,就需要使用 @ComponentScan 显式指定扫描包
// 正确配置启动类(scanBasePackages = \"com.ruoyi\")(\"com.ruoyi.**.mapper\")
- 动态表名导致自动填充失效,实体类字段未正确配置自动填充策略
public void addTaskWithDynamicTable(实体类 task) { // 先设置动态表名 MybatisPlusUtils.setDynamicTableName(\"表名\"); // 再手动设置时间(双重保障) task.setCreateTime(new Date()); task.setUpdateTime(new Date()); mapper类.insert(task);}#避免措施:实体类字段使用正确注解(fill = FieldFill.INSERT) private Date createTime;
- 分页插件冲突问题,PageHelper 与 MyBatis-Plus 分页不兼容
原因:同时存在两套分页机制,最好是不要改动原来的依赖,然后引入新的mybatis-plus即可