大家好,我是java1234_小锋老师。

2025 年 11 月 30 日,MyBatis-Plus 发布了
3.5.15。这次更新不大,但两件事很实在:能跟 Spring Boot 4.0 一起跑了,JSON 字段也能用上 Jackson 3。
先说一句:MyBatis-Plus 是干什么的
如果你写过纯 MyBatis,大概体会过这种感觉:一张表的增删改查,Mapper 接口加 XML 能写老长一截。条件一多,XML 里全是 <if test>,字段名改了还得全文搜。
MyBatis-Plus 就是在 MyBatis 上面加了一层常用能力。官方那句「只做增强、不做改变」听着像口号,实际体验很直接:原来怎么写 XML,现在还能怎么写;不想写的部分,交给它。
日常用得最多的大概就这几样:
BaseMapper:单表增删改查不用自己写LambdaQueryWrapper:条件用User::getUsername这种方式写,字段名写错编译期就能发现- 分页插件:你查列表,它帮你拼分页 SQL
- 代码生成器:表结构出来后,Entity、Mapper、Service 一把生成
说白了,它不是要替换 MyBatis,而是把那些每个项目都在重复写的东西收掉。

这次版本到底改了什么
3.5.15 的更新日志不长。真正会碰到手的,主要是这两项:
一是跟上 Spring Boot 4.0。
Boot 4 不是小版本升级,Starter 的 Maven 坐标都换了。以前 Boot 3 那套 mybatis-plus-spring-boot3-starter 直接丢进 Boot 4 项目,自动配置对不上。这次官方把 Boot 4 的 Starter 补齐了。
二是支持 Jackson 3。
Jackson 3 把核心包从 com.fasterxml.jackson 挪到了 tools.jackson,老的 JacksonTypeHandler 吃的还是 Jackson 2。新项目如果已经上 Jackson 3,JSON 列就该用新的 Jackson3TypeHandler。
除此之外还有几处边角,用得到再关心就行:代码生成器的元数据构建调整了一下;Enjoy 模板生成 XML 的 bug 修了;CrudRepository 批量操作时,不在事务里会更及时地关掉连接。
两件大事凑在一个小版本里,对准备尝鲜 Boot 4 的人其实挺友好------Mapper 还是那个 Mapper,不用重新学一套写法。

整条链路可以看成这样:
#mermaid-svg-Y83KxpjobLhh96lk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Y83KxpjobLhh96lk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Y83KxpjobLhh96lk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Y83KxpjobLhh96lk .error-icon{fill:#552222;}#mermaid-svg-Y83KxpjobLhh96lk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Y83KxpjobLhh96lk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Y83KxpjobLhh96lk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Y83KxpjobLhh96lk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Y83KxpjobLhh96lk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Y83KxpjobLhh96lk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Y83KxpjobLhh96lk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Y83KxpjobLhh96lk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Y83KxpjobLhh96lk .marker.cross{stroke:#333333;}#mermaid-svg-Y83KxpjobLhh96lk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Y83KxpjobLhh96lk p{margin:0;}#mermaid-svg-Y83KxpjobLhh96lk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Y83KxpjobLhh96lk .cluster-label text{fill:#333;}#mermaid-svg-Y83KxpjobLhh96lk .cluster-label span{color:#333;}#mermaid-svg-Y83KxpjobLhh96lk .cluster-label span p{background-color:transparent;}#mermaid-svg-Y83KxpjobLhh96lk .label text,#mermaid-svg-Y83KxpjobLhh96lk span{fill:#333;color:#333;}#mermaid-svg-Y83KxpjobLhh96lk .node rect,#mermaid-svg-Y83KxpjobLhh96lk .node circle,#mermaid-svg-Y83KxpjobLhh96lk .node ellipse,#mermaid-svg-Y83KxpjobLhh96lk .node polygon,#mermaid-svg-Y83KxpjobLhh96lk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Y83KxpjobLhh96lk .rough-node .label text,#mermaid-svg-Y83KxpjobLhh96lk .node .label text,#mermaid-svg-Y83KxpjobLhh96lk .image-shape .label,#mermaid-svg-Y83KxpjobLhh96lk .icon-shape .label{text-anchor:middle;}#mermaid-svg-Y83KxpjobLhh96lk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Y83KxpjobLhh96lk .rough-node .label,#mermaid-svg-Y83KxpjobLhh96lk .node .label,#mermaid-svg-Y83KxpjobLhh96lk .image-shape .label,#mermaid-svg-Y83KxpjobLhh96lk .icon-shape .label{text-align:center;}#mermaid-svg-Y83KxpjobLhh96lk .node.clickable{cursor:pointer;}#mermaid-svg-Y83KxpjobLhh96lk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Y83KxpjobLhh96lk .arrowheadPath{fill:#333333;}#mermaid-svg-Y83KxpjobLhh96lk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Y83KxpjobLhh96lk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Y83KxpjobLhh96lk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y83KxpjobLhh96lk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Y83KxpjobLhh96lk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y83KxpjobLhh96lk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Y83KxpjobLhh96lk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Y83KxpjobLhh96lk .cluster text{fill:#333;}#mermaid-svg-Y83KxpjobLhh96lk .cluster span{color:#333;}#mermaid-svg-Y83KxpjobLhh96lk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Y83KxpjobLhh96lk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Y83KxpjobLhh96lk rect.text{fill:none;stroke-width:0;}#mermaid-svg-Y83KxpjobLhh96lk .icon-shape,#mermaid-svg-Y83KxpjobLhh96lk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Y83KxpjobLhh96lk .icon-shape p,#mermaid-svg-Y83KxpjobLhh96lk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Y83KxpjobLhh96lk .icon-shape .label rect,#mermaid-svg-Y83KxpjobLhh96lk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Y83KxpjobLhh96lk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Y83KxpjobLhh96lk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Y83KxpjobLhh96lk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Spring Boot 4.0 应用
mybatis-plus-spring-boot4-starter
MyBatis-Plus 3.5.15
BaseMapper / Wrapper
Jackson3TypeHandler
t_ 业务表
Spring Boot 4 项目怎么接
先记住一件事:Boot 版本和 Starter 是绑死的,别混用。
| 你用的 Spring Boot | 该引的 Starter |
|---|---|
| 2.x | mybatis-plus-boot-starter |
| 3.x | mybatis-plus-spring-boot3-starter |
| 4.x | mybatis-plus-spring-boot4-starter |
名字差一个数字,自动配置完全不是一套。Boot 4 项目里继续引 Boot 3 的包,启动阶段就会很尴尬。
Maven 依赖
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0</version>
</parent>
<dependencies>
<!-- 注意:这里是 boot4,不是 boot3 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
<version>3.5.15</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies>
数据源随便配一份
yaml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/db_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
实体、Mapper、查一条数据
写法跟以前几乎一样。下面这个例子对应表 t_user,没使用 Lombok,getter / setter 自己写。
java
/**
* 用户实体,对应表 t_user
*/
@TableName("t_user")
public class User {
/** 主键 */
@TableId(type = IdType.AUTO)
private Long id;
/** 用户名 */
private String username;
/** 密码,示例环境用明文 123456 */
private String password;
/** 性别,默认男 */
private String gender;
/** 创建时间 */
private LocalDateTime createTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
}
Mapper 还是空接口,继承 BaseMapper 就够用:
java
/**
* 用户表 Mapper
*/
public interface UserMapper extends BaseMapper<User> {
}
查用户名对应的那一条,用 Lambda 写条件就行:
java
/**
* 按用户名查询用户
*/
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
/**
* 用方法引用写条件,少手敲列名
*/
public User findByUsername(String username) {
return userMapper.selectOne(new LambdaQueryWrapper<User>()
.eq(User::getUsername, username)
.last("LIMIT 1"));
}
}
启动类、@MapperScan 这些,跟 Boot 3 没多大差别。真正要改的,往往就那一行依赖:换成 mybatis-plus-spring-boot4-starter。
JSON 字段换成 Jackson 3
很多表会留一列存扩展信息,比如用户的城市、标签,不想为此再拆一张表。以前 Jackson 2 用 JacksonTypeHandler,现在 Jackson 3 对应的是 Jackson3TypeHandler。
它干的事情很朴素:写入时把对象变成 JSON 字符串,查出来再转回对象。

先准备一个扩展信息类
java
/**
* 用户扩展信息,最终会写成 JSON 存进 t_user.extra_info
*/
public class ExtraInfo {
/** 城市 */
private String city;
/** 标签列表 */
private List<String> tags;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public List<String> getTags() {
return tags;
}
public void setTags(List<String> tags) {
this.tags = tags;
}
}
挂到实体字段上
这里有个容易漏的地方:@TableName 上要加 autoResultMap = true。不加的话,查出来的 JSON 列不会走类型处理器,字段可能是空的,或者还是原始字符串。
java
/**
* 带 JSON 扩展字段的用户实体
*/
@TableName(value = "t_user", autoResultMap = true)
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
/**
* 用 Jackson 3 读写 extra_info 列
*/
@TableField(typeHandler = Jackson3TypeHandler.class)
private ExtraInfo extraInfo;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public ExtraInfo getExtraInfo() {
return extraInfo;
}
public void setExtraInfo(ExtraInfo extraInfo) {
this.extraInfo = extraInfo;
}
}
插入一条看看
java
/**
* 保存带 JSON 扩展信息的用户
*/
public void saveUser() {
ExtraInfo extraInfo = new ExtraInfo();
extraInfo.setCity("上海");
extraInfo.setTags(Arrays.asList("vip", "spring-boot4"));
User user = new User();
user.setUsername("admin");
user.setPassword("123456");
user.setGender("男");
user.setExtraInfo(extraInfo);
userMapper.insert(user);
}
插进去之后,extra_info 列大概长这样:
json
{"city":"上海","tags":["vip","spring-boot4"]}
项目里如果已经配过自己的 Jackson 3 ObjectMapper(日期格式、忽略空字段之类),最好在启动时塞给 Handler,别让它再 new 一份默认的:
java
/**
* 复用 Spring 容器里的 ObjectMapper
*/
@Configuration
public class Jackson3Config {
public Jackson3Config(ObjectMapper objectMapper) {
Jackson3TypeHandler.setObjectMapper(objectMapper);
}
}
还有个包名问题,建议复制的时候盯一眼:Jackson 3 的 ObjectMapper 在 tools.jackson.databind 下面。如果手滑 import 成了 com.fasterxml.jackson.databind.ObjectMapper,编译也许能过,跑起来 TypeHandler 会对不上。
升级时容易踩的坑
Starter 选错。
这是最常见的。Boot 4 必须用 mybatis-plus-spring-boot4-starter。包能下下来不代表能启动,自动配置对不上时,报错往往不在 MyBatis-Plus 自己身上,排查会绕一会儿。
JSON 处理器和 Jackson 版本要匹配。
实体写了 Jackson3TypeHandler,classpath 里就得是 Jackson 3。老项目还停在 Jackson 2,继续用原来的 JacksonTypeHandler 就行,没必要为了新类名硬升。
启动时报 factoryBeanObjectType。
有人在 3.5.15 + Boot 4 组合下碰到过:
text
Invalid value type for attribute 'factoryBeanObjectType': java.lang.String
多半是带进来的 mybatis-spring 偏旧。可以在 pom 里显式指定 org.mybatis:mybatis-spring:4.0.0。后面的 3.5.16 已经在 Boot 4 Starter 里把这个依赖升上去了,生产环境如果卡在这,升级一小步往往比自己排除依赖更省事。
用了代码生成器。
这次元数据构建有调整,Enjoy 模板生成 XML 的问题也修了。升完版本后,拿一张表重新生成一次,跟手里现有的 Mapper XML 对比一下更稳妥。
写在最后
3.5.15 不像那种铺开讲新 API 的大版本。它更像把两扇门打开:一边是 Spring Boot 4,一边是 Jackson 3。门后的用法还是熟悉的 BaseMapper、Lambda 条件和分页。
新项目直接上 mybatis-plus-spring-boot4-starter;JSON 列如果已经跟 Jackson 3 走,换成 Jackson3TypeHandler。老项目没必要为了版本号本身去升,等真正要上 Boot 4 的时候再动,节奏更轻松。