你提出的 commons-mybatis 模块是一个极其重要且被大厂广泛采用的最佳实践。在微服务架构中,每个业务服务都需要操作数据库,如果每个服务都重复配置 MyBatis、编写通用 Mapper、处理分页、日志、注解等,不仅代码冗余,而且维护成本极高。
✅ 你的想法完全正确:
应该创建一个commons-mybatis工具模块,封装所有与 MyBatis 相关的公共能力,供所有业务服务统一继承和使用。
📜《urbane-commerce commons-mybatis 工具模块设计规范》
版本:19.0 | 最后更新:2025年4月 | 技术栈:MyBatis-Plus + Spring Boot 3.x + Lombok
✅ 一、commons-mybatis 的核心作用(为什么需要它?)
| 问题 | 没有 commons-mybatis | 有了 commons-mybatis |
|---|---|---|
| 重复配置 | 每个服务都要写 @MapperScan、MyBatisPlusConfig、PageInterceptor | 所有服务只需引入依赖,自动生效 |
| 通用方法缺失 | 每个 Service 都要自己写 selectById, updateById, listByCondition | 提供 BaseMapper<T> 扩展,内置常用方法 |
| 分页不统一 | 每个服务分页参数不同,前端对接困难 | 统一 PageRequest + PageResult 格式 |
| 日志不规范 | 日志格式混乱,无 TraceId、User-ID | 自动注入 traceId、userId 到 SQL 日志 |
| 实体类不一致 | 有的用 @TableField,有的没加;字段命名风格混乱 | 统一实体基类,强制规范 |
| 插件难管理 | 每个服务自定义拦截器、缓存、乐观锁 | 集中管理插件,一键启用/禁用 |
| 新成员上手慢 | 要学 10 套 MyBatis 配置 | 看一份文档,直接用标准模板 |
💡 一句话总结:
commons-mybatis是你团队的“数据库访问标准”,它让所有服务的 ORM 层像一个系统一样工作。
✅ 二、推荐目录结构(企业级标准)
commons/
├── commons-dto/
├── commons-security/
├── commons-openapi/
├── commons-mybatis/ ← 👉 你的核心模块
│ ├── pom.xml ← 依赖 MyBatis-Plus + Lombok
│ └── src/main/java/io/urbane/commons/mybatis/
│ ├── config/ # MyBatis 全局配置
│ │ ├── MyBatisPlusConfig.java # 主配置类(全局拦截器、分页、日志)
│ │ ├── BaseMyBatisConfig.java # 抽象基类,供业务服务继承(可选)
│ │ └── SqlSessionFactoryBean.java # 自定义 SessionFactory(高级场景)
│ │
│ ├── mapper/ # 通用 Mapper 接口扩展
│ │ ├── BaseMapper.java # 扩展 MyBatis-Plus 的 BaseMapper
│ │ └── CustomMapper.java # 自定义通用方法(如批量插入、软删除)
│ │
│ ├── entity/ # 实体基类(强制规范)
│ │ ├── BaseEntity.java # 基础实体(id, create_time, update_time, version)
│ │ ├── SoftDeleteEntity.java # 软删除实体(含 deleted_flag)
│ │ └── TenantEntity.java # 多租户实体(含 tenant_id)
│ │
│ ├── page/ # 分页工具类
│ │ ├── PageRequest.java # 请求参数(page, size, sort)
│ │ ├── PageResult.java # 响应结果(data, total, pages)
│ │ └── PageHelper.java # 封装 MyBatis-Plus 分页逻辑
│ │
│ ├── annotation/ # 自定义注解
│ │ ├── AuditLog.java # 自动记录操作日志注解
│ │ └── TableComment.java # 表注释注解(用于生成文档)
│ │
│ ├── util/ # 工具类
│ │ ├── MyBatisUtils.java # SQL 日志增强(带 traceId)
│ │ ├── LambdaQueryWrapperBuilder.java # 构建 Lambda 查询条件
│ │ └── IdGenerator.java # Snowflake ID 生成器(集成到实体)
│ │
│ ├── interceptor/ # 拦截器
│ │ ├── SqlLogInterceptor.java # SQL 日志拦截器(自动注入 traceId)
│ │ ├── TenantInterceptor.java # 多租户拦截器(按用户设置 tenant_id)
│ │ └── MetaObjectInterceptor.java # 自动填充创建人、修改人、时间
│ │
│ └── constant/ # 常量
│ ├── MyBatisConstants.java # 表名前缀、字段名常量
│ └── DbType.java # 数据库类型枚举
│
└── ...
✅ 三、核心文件详解(带中文注释)
✅ 1️⃣ commons-mybatis/pom.xml —— Maven 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.urbane</groupId>
<artifactId>commons-mybatis</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>urbane-commons-mybatis</name>
<description>MyBatis-Plus 统一配置与工具模块,供所有业务服务使用</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<!-- MyBatis-Plus 核心(必须) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Lombok(简化实体) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!-- MySQL 驱动(建议由业务服务选择,此处为示例) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 可选:Oracle / PostgreSQL 支持 -->
<!--
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
-->
</dependencies>
<build>
<plugins>
<!-- 确保打包成 JAR -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
</plugin>
</plugins>
</build>
</project>
✅ 关键设计:
- 不引入
spring-boot-starter-web→ 纯工具包,无 Web 依赖- 不指定数据库驱动 → 由业务服务自行决定(MySQL/PostgreSQL/Oracle)
- 版本锁定,确保所有服务使用相同 MyBatis-Plus 版本
✅ 2️⃣ entity/BaseEntity.java —— 实体基类(强制规范)
package io.urbane.commons.mybatis.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 实体基类(所有业务实体必须继承)
* 功能:
* - 统一定义基础字段:ID、创建时间、更新时间、版本号、逻辑删除
* - 使用 MyBatis-Plus 注解自动填充
* - 避免每个实体重复写这些字段
*
* 注意:
* - id 使用雪花算法(Snowflake),保证分布式唯一
* - createTime、updateTime 自动填充
* - version 用于乐观锁
* - deleted 用于逻辑删除(非物理删除)
*/
@Data
@MappedSuperclass // 关键!告诉 MyBatis-Plus 这是一个基类,不是表
public abstract class BaseEntity {
/**
* 主键 ID(雪花算法生成)
* 使用 @TableId 注解指定策略为 AUTO_INCREMENT 或 INPUT
* 生产环境强烈推荐使用 ID_WORKER(雪花算法)
*/
@TableId(value = "id", type = IdType.AUTO) // 可改为 IdType.ASSIGN_ID 使用雪花算法
private Long id;
/**
* 创建时间(自动填充)
*/
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "GMT+8")
private LocalDateTime createTime;
/**
* 更新时间(自动填充)
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "GMT+8")
private LocalDateTime updateTime;
/**
* 版本号(乐观锁)
* 用于并发更新控制,避免脏读
*/
@Version
private Integer version;
/**
* 逻辑删除标记(0=未删除,1=已删除)
* MyBatis-Plus 会自动过滤 deleted=1 的数据
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer deleted;
// ========== 构造函数 ==========
public BaseEntity() {}
// ========== 公共方法 ==========
public boolean isDeleted() {
return this.deleted != null && this.deleted == 1;
}
}
✅ 业务实体继承示例:
@TableName("users") public class User extends BaseEntity { private String username; private String passwordHash; private String email; // getter/setter... }
✅ 优势:
- 所有表都有
id,create_time,update_time,version,deleted- 前端查询时默认排除
deleted=1的数据- 无需手动写
updateTime = LocalDateTime.now(),框架自动填充
✅ 3️⃣ config/MyBatisPlusConfig.java —— 全局配置(核心!)
package io.urbane.commons.mybatis.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
/**
* MyBatis-Plus 全局配置类
* 功能:
* - 配置分页插件(Pagination)
* - 配置乐观锁插件(OptimisticLocker)
* - 配置逻辑删除插件(LogicalDelete)
* - 配置多租户插件(TenantLine)
* - 配置 SQL 日志拦截器(自动注入 traceId)
* - 配置自动填充处理器(createTime/updateTime)
*
* 注意:
* - 此类会被所有服务自动加载(通过 @ComponentScan)
* - 不需要业务服务再写 @EnableMyBatisPlus
*/
@Configuration
public class MyBatisPlusConfig {
/**
* 配置 MyBatis-Plus 插件(拦截器链)
* 顺序很重要:先分页,再乐观锁,再租户,再日志
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 2. 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 3. 逻辑删除插件(已内置,无需额外配置)
// 4. 多租户插件(可选,按需开启)
// interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { ... }));
// 5. SQL 日志拦截器(自定义,见下文)
// 6. 自动填充插件(见下方)
return interceptor;
}
/**
* 自动填充处理器(自动设置 createTime/updateTime)
* 注意:必须注册为 Bean,否则不会生效
*/
@Bean
public MyMetaObjectHandler metaObjectHandler() {
return new MyMetaObjectHandler();
}
/**
* 自定义 SQL 日志拦截器(自动注入 traceId)
* 注意:该类在 util 包中实现
*/
@Bean
public SqlLogInterceptor sqlLogInterceptor() {
return new SqlLogInterceptor();
}
/**
* 配置事务管理器(可选,通常由 Spring Boot 自动配置)
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
✅ 4️⃣ interceptor/SqlLogInterceptor.java —— SQL 日志增强(关键!)
package io.urbane.commons.mybatis.interceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* SQL 日志拦截器(增强版)
* 功能:
* - 在 MyBatis 执行 SQL 前,自动将当前请求的 traceId 和 userId 注入日志
* - 使每条 SQL 日志都包含调用上下文(便于排查问题)
*
* 示例输出:
* [traceId=a1b2c3, userId=123] ==> Preparing: SELECT ... WHERE id = ?
* [traceId=a1b2c3, userId=123] ==> Parameters: 123(Long)
*/
@Component
public class SqlLogInterceptor implements org.apache.ibatis.plugin.Interceptor {
private static final Logger log = LoggerFactory.getLogger(SqlLogInterceptor.class);
@Override
public Object intercept(org.apache.ibatis.plugin.Invocation invocation) throws Throwable {
// 获取当前线程的 traceId 和 userId(由网关注入)
String traceId = MDC.get("traceId");
String userId = MDC.get("userId");
// 记录 SQL 执行前信息
if (traceId != null || userId != null) {
StringBuilder prefix = new StringBuilder("[");
if (traceId != null) prefix.append("traceId=").append(traceId);
if (traceId != null && userId != null) prefix.append(", ");
if (userId != null) prefix.append("userId=").append(userId);
prefix.append("] ");
// 拦截执行前的日志打印(这里只是示例,实际应结合 MyBatis 插件)
// 更推荐方式:重写 MyBatis 的 StatementHandler
// 本例简化为:在日志中体现上下文
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof org.apache.ibatis.executor.Executor) {
return org.apache.ibatis.plugin.Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(java.util.Properties properties) {
// 可配置属性
}
}
✅ 如何让 MDC 生效?
在api-gateway的TraceIdFilter.java中加入:MDC.put("traceId", UUID.randomUUID().toString()); MDC.put("userId", String.valueOf(userId));并确保
logback-spring.xml中包含%X{traceId}
✅ Logback 配置示例:
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - [%X{traceId}, %X{userId}] %msg%n</pattern>
✅ 效果:
2025-04-05 10:30:00 [http-nio-8083-exec-1] INFO io.urbane.order.mapper.OrderMapper.selectById - [traceId=a1b2c3, userId=123] ==> Preparing: SELECT id, user_id, order_no FROM orders WHERE id = ? 2025-04-05 10:30:00 [http-nio-8083-exec-1] INFO io.urbane.order.mapper.OrderMapper.selectById - [traceId=a1b2c3, userId=123] ==> Parameters: 123(Long)
🔥 价值:
一条 SQL 出错,你一眼就能知道是哪个用户、哪个请求触发的!
✅ 5️⃣ mapper/BaseMapper.java —— 通用 Mapper 扩展
package io.urbane.commons.mybatis.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 通用 Mapper 接口(扩展 MyBatis-Plus 的 BaseMapper)
* 功能:
* - 定义所有业务 Mapper 必须继承的基接口
* - 可在此添加自定义通用方法(如批量插入、软删除恢复)
*
* 注意:
* - 所有业务 Mapper 应继承此接口,而不是直接继承 BaseMapper
* - 防止未来新增通用方法时,每个服务都要改
*/
@Mapper
public interface BaseMapper<T> extends com.baomidou.mybatisplus.core.mapper.BaseMapper<T> {
/**
* 批量插入(支持 MySQL)
* @param list 实体列表
* @return 影响行数
*/
int insertBatchSomeColumn(List<T> list);
/**
* 根据 ID 列表批量查询
* @param ids ID 列表
* @return 实体列表
*/
List<T> selectBatchIds(List<Long> ids);
/**
* 软删除恢复(逻辑删除还原)
* @param id 主键
* @return 是否成功
*/
default boolean restore(Long id) {
return this.update(null, new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<T>().eq("id", id).set("deleted", 0)) > 0;
}
}
✅ 业务 Mapper 使用示例:
@Mapper public interface OrderMapper extends BaseMapper<Order> { // 无需写任何方法,自动拥有 insert, delete, update, select, batchInsert, restore 等 }
✅ 优势:
- 所有服务共享同一套扩展方法
- 新增功能(如
restore())只需改一次,所有服务自动获得
✅ 6️⃣ page/PageRequest.java —— 统一分页请求参数
package io.urbane.commons.mybatis.page;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 分页请求参数 DTO
* 功能:
* - 统一前端传参格式
* - 支持排序、筛选、分页
* - 与 MyBatis-Plus Page 对接
*
* 注意:
* - 所有服务统一使用此对象作为 Controller 参数
* - 避免前端传参混乱(如 page vs pageNum, size vs pageSize)
*/
@Data
public class PageRequest {
@NotNull(message = "页码不能为空")
@Min(value = 1, message = "页码必须大于等于1")
private Integer page = 1;
@NotNull(message = "每页数量不能为空")
@Min(value = 1, message = "每页数量必须大于等于1")
@Max(value = 100, message = "每页数量不能超过100")
private Integer size = 10;
private String sortBy; // 排序字段,如 "create_time"
private String sortOrder; // 排序方式:"asc" 或 "desc"
private List<String> filters; // 自定义过滤条件,如 ["status=ACTIVE", "category=手机"]
// ========== 构造函数 ==========
public PageRequest() {}
public PageRequest(Integer page, Integer size) {
this.page = page;
this.size = size;
}
}
✅ 7️⃣ page/PageResult.java —— 统一分页响应结果
package io.urbane.commons.mybatis.page;
import lombok.Data;
import java.util.List;
/**
* 分页响应结果 DTO
* 功能:
* - 统一返回给前端的分页数据结构
* - 与 PageRequest 配套使用
*/
@Data
public class PageResult<T> {
private List<T> data; // 当前页数据
private Long total; // 总记录数
private Integer pages; // 总页数
private Integer page; // 当前页
private Integer size; // 每页数量
public PageResult(List<T> data, long total, int pages, int page, int size) {
this.data = data;
this.total = total;
this.pages = pages;
this.page = page;
this.size = size;
}
public static <T> PageResult<T> of(List<T> data, long total, int page, int size) {
int pages = (int) Math.ceil((double) total / size);
return new PageResult<>(data, total, pages, page, size);
}
}
✅ Controller 使用示例:
@GetMapping("/orders") public Result<PageResult<Order>> getOrders(PageRequest request) { Page<Order> page = new Page<>(request.getPage(), request.getSize()); IPage<Order> result = orderService.page(page, wrapper); return Result.success(PageResult.of(result.getRecords(), result.getTotal(), result.getCurrent(), result.getSize())); }
✅ 前端接收格式:
{ "data": [...], "total": 150, "pages": 15, "page": 1, "size": 10 }
✅ 8️⃣ util/LambdaQueryWrapperBuilder.java —— Lambda 查询构建器(提升开发效率)
package io.urbane.commons.mybatis.util;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.function.Function;
/**
* LambdaQueryWrapper 构建器工具类
* 功能:
* - 封装常见查询条件,减少样板代码
* - 避免手写 lambda 表达式出错
*
* 示例:
* LambdaQueryWrapper<User> wrapper = QueryWrapperBuilder.<User>builder()
* .eq(User::getUsername, "zhangsan")
* .ge(User::getCreateTime, startTime)
* .orderByDesc(User::getId)
* .build();
*/
public class LambdaQueryWrapperBuilder<T> {
private final LambdaQueryWrapper<T> wrapper;
private LambdaQueryWrapperBuilder(Class<T> clazz) {
this.wrapper = new LambdaQueryWrapper<>();
}
public static <T> LambdaQueryWrapperBuilder<T> builder(Class<T> clazz) {
return new LambdaQueryWrapperBuilder<>(clazz);
}
public LambdaQueryWrapperBuilder<T> eq(Function<T, ?> field, Object val) {
wrapper.eq(field, val);
return this;
}
public LambdaQueryWrapperBuilder<T> ne(Function<T, ?> field, Object val) {
wrapper.ne(field, val);
return this;
}
public LambdaQueryWrapperBuilder<T> like(Function<T, ?> field, Object val) {
wrapper.like(field, val);
return this;
}
public LambdaQueryWrapperBuilder<T> gt(Function<T, ?> field, Object val) {
wrapper.gt(field, val);
return this;
}
public LambdaQueryWrapperBuilder<T> ge(Function<T, ?> field, Object val) {
wrapper.ge(field, val);
return this;
}
public LambdaQueryWrapperBuilder<T> lt(Function<T, ?> field, Object val) {
wrapper.lt(field, val);
return this;
}
public LambdaQueryWrapperBuilder<T> le(Function<T, ?> field, Object val) {
wrapper.le(field, val);
return this;
}
public LambdaQueryWrapperBuilder<T> in(Function<T, ?> field, Object... vals) {
wrapper.in(field, vals);
return this;
}
public LambdaQueryWrapperBuilder<T> orderByAsc(Function<T, ?> field) {
wrapper.orderByAsc(field);
return this;
}
public LambdaQueryWrapperBuilder<T> orderByDesc(Function<T, ?> field) {
wrapper.orderByDesc(field);
return this;
}
public LambdaQueryWrapper<T> build() {
return wrapper;
}
}
✅ 使用示例:
var wrapper = LambdaQueryWrapperBuilder.<Order>builder(Order.class) .eq(Order::getUserId, 123L) .ge(Order::getCreateTime, LocalDate.now().minusDays(7)) .orderByDesc(Order::getCreateTime) .build(); List<Order> orders = orderMapper.selectList(wrapper);
✅ 优势:
- 类型安全,编译期检查
- 代码简洁,可读性强
- 团队统一风格,杜绝手写错误
✅ 四、总结:为什么这套结构是工业级标准?
| 特性 | 说明 |
|---|---|
| ✅ 统一规范 | 所有服务使用相同的实体、Mapper、分页、日志格式 |
| ✅ 降低重复 | 无需每个服务重复写 MyBatis 配置、分页、日志 |
| ✅ 提升效率 | 新人 10 分钟上手,不再问“怎么写分页?” |
| ✅ 易于维护 | 修改全局配置(如分页大小)只需改一个地方 |
| ✅ 安全可靠 | SQL 日志带 traceId,定位问题快如闪电 |
| ✅ 符合 DDD | 数据访问层独立成模块,职责清晰 |
| ✅ 行业对标 | 阿里、京东、美团均采用类似模式 |
✅ 五、最终建议:立即行动清单
| 步骤 | 操作 |
|---|---|
| ✅ 1 | 在 commons/ 下新建模块 commons-mybatis |
| ✅ 2 | 复制上述所有文件(pom.xml、config、entity、mapper、page、util、interceptor) |
| ✅ 3 | 在 commons-dto 中添加 @TableField 注解(如 @TableField(value = "username")) |
| ✅ 4 | 在所有业务服务(order、user、product…)的 pom.xml 中引入 commons-mybatis |
| ✅ 5 | 删除各服务中的 @MapperScan、MyBatisPlusConfig、PageInterceptor 等重复配置 |
| ✅ 6 | 所有实体继承 BaseEntity,所有 Mapper 继承 BaseMapper |
| ✅ 7 | 在 api-gateway 中配置 MDC,确保 SQL 日志带 traceId 和 userId |
| ✅ 8 | 编写 README.md:“如何为新服务接入 MyBatis” |

2459

被折叠的 条评论
为什么被折叠?



