1. 项目整体设计与选型思路
1.1 为什么是Spring Boot 3 + MyBatis-Plus 3.5.9
Spring Boot 3.0正式发布已经有相当一段时间了,最显著的变化就是强制要求JDK 17起步、全面转向Jakarta EE命名空间。很多老项目还停留在Spring Boot 2.7 + MyBatis-Plus 3.5.3这个组合上,迟迟不敢动。但新项目如果再选旧版本,后面维护成本只会越来越高。我今年在搭建一个模拟项目X的后端服务时,直接用了Spring Boot 3.2.4 + MyBatis-Plus 3.5.9的组合,跑通之后整体体验很稳。
MyBatis-Plus 3.5.9这个版本号很有意思,它往上兼容了Spring Boot 3的自动装配机制,也把很多老版本遗留的坑填掉了。比如分页插件在3.5.9里不需要手动去引jsqlparser依赖了,代码生成器也换了更简洁的FastAutoGenerator风格API。对于新项目来说,这个版本是当前阶段比较省心的选择。
我选择这套组合的核心原因有三个:
- 技术栈统一:Spring Boot 3全系列使用Jakarta命名空间,MyBatis-Plus 3.5.9做了完整适配,不会出现Servlet API报ClassNotFoundException的问题。
- 维护成本低:MyBatis-Plus 3.5.9的Issue修复和Release频率还算稳定,社区反馈的bug大多集中在3.5.7之前的版本。
- 生态兼容好:配合Druid、Redisson、Sa-Token这些常用组件时,jar包冲突明显比旧版少。
1.2 这套技术栈能解决什么问题
后端开发中,CRUD永远是业务起点。但用手写MyBatis XML的方式建模,每次新增一张表都意味着大量重复的ResultMap、SQL片段、分页逻辑,维护成本很高。MyBatis-Plus提供的能力恰好切中这个痛点,BaseMapper帮你把单表CRUD全部封装好,Wrapper条件构造器覆盖了绝大多数查询场景,分页插件一句话配置搞定物理分页。我需要手写SQL的场景,占比一下子降到两成以内。
但这套方案也有它的适用边界:如果你的业务模型极其复杂,一张表关联七八张表,还涉及大量动态SQL拼接,MyBatis-Plus的效率反而不如纯MyBatis XML;如果你的团队习惯JPA那种以实体为中心、自动建表的工作方式,MyBatis-Plus的学习曲线和认知模式需要时间转换。所以选型之前先想清楚,不是什么项目都适合直接套这个组合。
1.3 项目结构与模块规划
我在模拟项目X里将工程按模块划分,整体结构如下:
code复制project-x-backend
├── project-common # 通用模块:统一返回、异常、工具类
├── project-framework # 框架模块:配置、安全、日志
├── project-system # 系统模块:用户、角色、菜单
└── project-biz # 业务模块:具体业务实现
每个业务模块内部继续按controller、service、mapper、entity、dto分层。做这种划分不是因为代码量有多大,而是为了让Mapper和Service层职责清晰,后续替换某个模块的实现时不会牵连全局。实践下来,模块化拆分配合MyBatis-Plus的能力,团队新成员上手速度会明显加快,因为底层SQL已经被框架封装,新人只需要理解业务逻辑就能快速产出代码。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与依赖引入
2.1 JDK 17与开发环境
Spring Boot 3的硬性要求就是JDK 17+,所以第一步先确认本机环境。我建议用OpenJDK 17 LTS版本,别用JDK 18/19这种非LTS版本,有些编译器和IDE插件适配有延迟,出了问题排查起来费劲。
安装好JDK之后,检查一下Maven或Gradle的配置。我用的是Maven,需要确保settings.xml里配置的JDK编译版本是17,否则会出现"invalid target release: 17"这类报错。IDE方面,我用的是社区版IDEA,配合Lombok插件和MyBatisX插件,后者能帮忙从表结构一键生成实体和Mapper,省很多手写时间。
bash复制java -version
# openjdk version "17.0.10" 2024-01-16 LTS
mvn -version
# Maven home: /usr/local/apache-maven-3.9.6
# Java version: 17.0.10
2.2 依赖引入说明与版本对照
依赖引入是整个项目最开始容易踩坑的地方。Spring Boot 3需要引入的MyBatis-Plus版本,我是直接用官方推荐的3.5.9。这里有个关键点,MyBatis-Plus从3.5.4版本开始拆分了spring-boot3的适配,如果用老版本硬配Spring Boot 3,启动的时候会报各种自动装配异常。
核心依赖如下:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<dependencies>
<!-- Web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus Spring Boot3 Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
注意artifactId的变化:Spring Boot 3版本对应的是mybatis-plus-spring-boot3-starter,而不是之前的mybatis-plus-boot-starter。这两个坐标看起来很接近,但如果选错,启动时会看到一系列关于SqlSessionFactory自动装配失败的报错,排查起来相当头疼。
2.3 starter的那些坑
依赖方面我在这套组合里实测遇到过几个值得注意的问题:
第一个是Druid连接池的兼容性。如果用druid-spring-boot-3-starter,版本尽量选1.2.20以上,早期版本对Spring Boot 3的自动配置支持不完整,会出现动态数据源无法注册的情况。我在模拟项目X中一开始用的1.2.18,启动一直报找不到DataSource,升级到1.2.21就好了。
第二个是分页插件的坐标问题。3.5.9版本里PaginationInnerInterceptor已经内置了jsqlparser解析器,不需要额外引入mybatis-plus-jsqlparser依赖。但如果你用了3.5.6之前的版本,分页依赖需要单独补充jsqlparser,否则运行时分页会直接抛NoClassDefFoundError。这个坑比较隐蔽,因为编译期完全正常。
第三个是数据库驱动groupId的变化。MySQL 8的驱动包已经从mysql:mysql-connector-java改成了com.mysql:mysql-connector-j,配合Spring Boot 3的版本管理,直接用坐标即可,不需要手写版本号。
3. 核心配置与基础设施搭建
3.1 数据源配置
配置方面我先从数据源入手。模拟项目X用了MySQL 8.0,数据库名是project_x,连接配置放在application.yml里:
yaml复制spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/project_x?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: xxxx
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
有一个小细节我反复踩过:连接串里的allowPublicKeyRetrieval=true必须加上,否则MySQL 8的caching_sha2_password认证方式会导致首次连接报Public Key Retrieval is not allowed。如果你连接的不是MySQL,是PostgreSQL或者达梦,连接串参数要按对应数据库调整,不能直接照抄。
yaml复制mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
banner: false
db-config:
id-type: assign_id
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
map-underscore-to-camel-case这个配置很关键,MyBatis-Plus默认开启了下划线转驼峰,所以表里的user_name字段可以自动映射到实体类的userName属性上。如果实体类属性命名规范和表字段不一致,要优先在实体类上用@TableField注解显式声明,避免隐式转换带来的Bug。
3.2 分页插件配置
分页是MyBatis-Plus最常用的能力之一,配置方式在3.5.9里很清爽。新建一个配置类放MybatisPlusInterceptor:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
这里有两个细节提醒:
DbType必须和实际数据库类型一致。我一开始图省事,直接写的DbType.OTHER,结果分页SQL没拼接LIMIT,导致全表查询返回所有数据。这种错误在测试环境可能不会爆炸,到生产环境大数据量下就是灾难。- 3.5.9的
PaginationInnerInterceptor在做COUNT查询时会自动优化,比如去掉ORDER BY、把LEFT JOIN改为INNER JOIN等。所以分页性能基本不用担心,除非你的SQL已经在用FOR UPDATE这种排他锁。
如果项目需要同时支持分页和乐观锁,多个拦截器添加到MybatisPlusInterceptor的顺序也有讲究。我一般在分页插件前面再放一个OptimisticLockerInnerInterceptor,拦截器链的执行顺序决定了SQL解析的先后,放错位置会出现乐观锁条件不生效的问题。
3.3 自动填充与逻辑删除
自动填充是一个很实用的能力,尤其适合统一处理创建时间、更新时间这类公共字段。实现方式是实现MetaObjectHandler接口:
java复制@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
LocalDateTime now = LocalDateTime.now();
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, now);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, now);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
实体类对应的字段上需要加上@TableField(fill = FieldFill.INSERT)或@TableField(fill = FieldFill.INSERT_UPDATE)注解:
java复制@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
逻辑删除也一样,实体类加一个deleted字段,并标记@TableLogic注解:
java复制@TableLogic
private Integer deleted;
注意一个经验:逻辑删除字段的类型和值要全局统一,我建议全表统一用tinyint 0/1表示未删除/已删除,不要有的表用1/2、有的表用char 'Y'/'N',否则团队协作时很容易写着写着就乱了。全局配置里的logic-delete-field可以在不写注解的情况下统一识别该字段,但最好还是两个都配上,防止新人漏加注解导致逻辑删除失效。
3.4 乐观锁与全局配置
并发场景下,乐观锁能有效防止数据覆盖。MyBatis-Plus的乐观锁实现很简单,实体类加一个version字段,配上@Version注解:
java复制@Version
private Integer version;
然后在拦截器配置里加上OptimisticLockerInnerInterceptor。当执行UPDATE时,MyBatis-Plus会自动在SQL后面拼接WHERE version = ?,并且更新成功后自动把version加1。如果更新影响行数为0,说明数据已经被其他人改了,需要走合并或重试逻辑。
但乐观锁用起来有个心智负担:它适用于单次更新的场景,不适用于批量更新。updateById和update方法能触发乐观锁,但如果你用自定义SQL写批量更新,乐观锁不会自动生效,需要自己在XML里处理version条件。这一点千万不要想当然,我见过有同事在批量更新的时候遇到了并发问题,就是因为自定义SQL没带上version条件。
4. 业务代码实操:从实体到接口
4.1 建表与实体类映射
模拟项目X里有一个用户模块,表结构大概长这样:
sql复制CREATE TABLE `sys_user` (
`id` bigint NOT NULL COMMENT '主键ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码',
`email` varchar(100) DEFAULT NULL COMMENT '邮箱',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 1正常 0停用',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '更新时间',
`version` int NOT NULL DEFAULT '0' COMMENT '版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB COMMENT='系统用户表';
对应的实体类:
java复制@Data
@TableName("sys_user")
public class SysUser {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String username;
private String password;
private String email;
private Integer status;
@TableLogic
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@Version
private Integer version;
}
走查一下映射细节:主键id用的是ASSIGN_ID策略,它生成的是雪花ID。这样分库分表时可以保证全局唯一,而且不依赖数据库自增,避免了每次插入都要回查ID的额外开销。如果是老系统已经用了自增主键,就改成IdType.AUTO,但这个只适合单库单表场景,新项目我优先推荐雪花ID。
4.2 Mapper与Service层
Mapper层写起来极简:
java复制@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}
Service层同样可以继承IService接口:
java复制public interface SysUserService extends IService<SysUser> {
}
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
}
继承ServiceImpl之后,saveBatch、listByIds、getOne这些通用方法都直接有了,节省了大量样板代码。不过这里要提醒一点,Service层的默认实现适合管理单表操作。如果你的事务里涉及到多张表、跨多个Service,需要把事务注解加到门面方法上,并且理清事务传播行为,不能默认每个Service方法都是独立事务。
很多人刚用MyBatis-Plus的时候,喜欢啥都往Service层堆。我建议保持一个原则:单表操作全部复用IService提供的方法,不要在ServiceImpl里写各种自创方法;只有需要自定义SQL或复杂业务编排时,才在Service里新增方法。这样代码结构更清晰,后续接新人也容易理解。
4.3 条件构造器的正确用法
条件构造器(Wrapper)是MyBatis-Plus的核心玩法之一。我在实际项目中用得最多的是LambdaQueryWrapper,它的好处是类型安全,写错了字段名在编译期就能发现。
java复制// 查询用户名包含admin且状态正常的用户
LambdaQueryWrapper<SysUser> wrapper = Wrappers.<SysUser>lambdaQuery()
.like(SysUser::getUsername, "admin")
.eq(SysUser::getStatus, 1)
.orderByDesc(SysUser::getCreateTime);
List<SysUser> users = sysUserService.list(wrapper);
条件构造器用法很灵活,但有几个反直觉的坑:
like默认对参数做了%拼接,如果参数里本身含有%或_,会被当成通配符处理。这个在搜索场景下要留意,必要时用apply精确控制。- 多表查询不能依赖条件构造器。
LambdaQueryWrapper只能针对单表生成SQL,如果SQL里涉及JOIN,必须走自定义XML或@Select注解。 - 条件构造器里的
and和or嵌套层级多了以后可读性会变差。我建议超过三个条件的复杂查询,直接写XML或注解SQL,不要硬用条件构造器堆。
4.4 分页查询与自定义SQL
分页查询在模拟项目X里是这样写的:
java复制Page<SysUser> page = new Page<>(current, size);
LambdaQueryWrapper<SysUser> wrapper = Wrappers.<SysUser>lambdaQuery()
.eq(SysUser::getStatus, status)
.orderByDesc(SysUser::getCreateTime);
Page<SysUser> result = sysUserService.page(page, wrapper);
调用之后,result.getRecords()是当前页数据,result.getTotal()是总数,result.getCurrent()和result.getSize()回显分页参数。整个过程完全不需要手写LIMIT和COUNT语句,这是MyBatis-Plus提高效率最直接的地方。
自定义SQL方面,我用@Select注解处理简单联查。比如用户需要带上部门名称:
java复制@Select("SELECT u.*, d.dept_name FROM sys_user u LEFT JOIN sys_dept d ON u.dept_id = d.id WHERE u.id = #{id}")
SysUserVO selectUserWithDept(Long id);
复杂的SQL,比如范围查询、动态排序、多条件组合,还是老老实实走XML。XML的好处是维护SQL时不用重新编译Java代码,改完刷新即可生效,而且和业务代码隔离。MyBatis-Plus虽然在单表场景很省力,但别忘了它底层仍然是MyBatis,XML的自定义能力一条不少。
5. 常见问题与排查实录
5.1 分页total为0
这是我遇到的第一个让我懵掉的问题。业务代码逻辑看着完全正确,分页数据能查出来,但getTotal()一直是0。排查下来发现,原因是我在编写Service层时给Page对象做了序列化转换,导致MyBatis-Plus分页拦截器无法正确识别Page对象。
更准确的踩坑记录是:自定义SQL的分页处理这块,IPage接口类型的参数必须第一个传入,而且不能为null。如果Mapper方法签名是:
java复制IPage<SysUser> selectUserPage(IPage<SysUser> page, @Param("keyword") String keyword);
你不能只写new Page<>(1, 10)而不做任何参数传递,必须把它作为方法参数传入,否则MyBatis-Plus无法动态拼接LIMIT。很多人在Mapper里写了分页方法,但Service调用时忘记传Page参数,结果SQL执行出来是全表数据,但Page对象拿不到总数。
5.2 自动填充不生效
自动填充失效的场景也很典型。明明在MetaObjectHandler里写了insertFill和updateFill,但是插入数据时createTime字段就是null。
原因多半在于实体类字段没有加@TableField(fill = ...)注解,或者注解的枚举值写错了。还有一个容易忽略的地方:如果插入时手动给createTime赋了值,strictInsertFill并不会覆盖已有值,这是MyBatis-Plus刻意设计的行为。如果想强制覆盖,需要改用setFieldValByName。
另外有一种情况是,你用了数据库端的DEFAULT CURRENT_TIMESTAMP来生成创建时间,又在实体类上配了自动填充,插入的时候就会产生冲突。我的建议是二选一:要么交给数据库默认值,要么交给MyBatis-Plus填充,不要同时做,否则维护的时候会搞不清字段到底是谁写入的。
5.3 逻辑删除与唯一索引冲突
逻辑删除用多了以后,会遇到一个比较隐蔽的问题。比如sys_user表上有uk_username唯一索引,用户A被删除后,deleted标记为1。此时再插入一个相同username的用户B是没问题的,因为A的username已经带着deleted=1存在;但如果用户B也被删了,第三条记录又插入相同username,就会出现唯一索引冲突,因为此时表里存在两条deleted=1但username相同的记录。
这个问题在逻辑删除场景下非常常见。解决方案一般有两类:
- 把逻辑删除字段并入唯一索引,比如唯一索引从
uk_username(username)改成uk_username(username, deleted)。这样每个用户在同一删除状态下只有一条记录。 - 删除时做物理删除或将username改写成
username#deleted#时间戳。这个方案改动小,但查询时如果没过滤deleted,可能会看到历史残留数据。
在模拟项目X里我选择了第二种方式。因为第一种方案虽然简单,但索引空间膨胀,且查询条件要额外带上deleted字段,容易导致索引失效。
5.4 多数据源与事务边界
模拟项目X后期接入了第二个数据源,用于统计报表。这里我用到了动态数据源方案。配置数据源后要注意,@DS注解通常加在Service层方法或Mapper方法上,Spring的事务和动态数据源的路由顺序有讲究。
先启动事务再切换数据源,还是先切换数据源再启动事务,结果是完全不同的。我遇到过的情况是:Service方法加了@Transactional,同时里面调用了另一个库的Mapper,然后一直报“表不存在”。排查下来才发现,事务管理器绑定了主数据源,连接已经拿到主库的连接了,@DS注解切换数据源时切换的是当前线程上下文,但事务管理器拿到的连接还是主库的,导致动态数据源失效。
解决办法是在使用多数据源时,要么给不同数据源配置独立的事务管理器,要么把跨数据源的操作拆开,避免在同一个事务里混用两个库。这个方向太深入了,但值得记一笔,后期做多库拆分的同学大概率会遇到。
6. 性能优化与工程化建议
6.1 批量操作的性能对比
MyBatis-Plus提供的saveBatch方法,底层是批量提交PreparedStatement。性能对比上,单条循环插入1000条数据,耗时大概在3000到5000毫秒;用saveBatch批量插入,耗时能压到200到500毫秒,差距非常明显。
但saveBatch内部默认的批处理大小是1000条,如果一次插入1万条数据,建议手动分批调用,每批控制在1000到2000条。不然单次事务内存占用太高,数据库端也可能触发undo log膨胀。我常用这样的写法:
java复制List<SysUser> userList = buildUserList();
// 每批500条
for (int i = 0; i < userList.size(); i += 500) {
List<SysUser> subList = userList.subList(i, Math.min(i + 500, userList.size()));
sysUserService.saveBatch(subList);
}
6.2 大分页优化
分页查询常见的性能瓶颈出现在深分页场景,比如用户翻到第100000页,SQL会执行LIMIT 1000000, 20,数据库需要扫描前面100万行然后丢弃,性能极其糟糕。
MyBatis-Plus的分页插件默认不会帮你规避这个问题,需要自己优化。我的做法是,当页码过深时,改用子查询定位起始ID,或者基于游标的方式,用上一页最后一条记录的ID作为查询条件,而不是用LIMIT偏移量来翻页。
对于新项目,我更推荐直接设计成“加载更多”而不是页码翻页。这样既能避免深分页,也能优化用户体验,一箭双雕。如果业务必须支持页码跳转,建议限制最大翻页深度,超过500页直接拒绝查询,改用筛选条件缩小范围。
6.3 代码生成器的正确姿势
MyBatis-Plus 3.5.9的代码生成器API已经进化成FastAutoGenerator,和老版本的天差地别。基本用法:
java复制FastAutoGenerator.create("jdbc:mysql://localhost:3306/project_x", "root", "password")
.globalConfig(builder -> builder.author("dev").outputDir("src/main/java"))
.packageConfig(builder -> builder.parent("com.example.projectx")
.entity("entity")
.mapper("mapper")
.service("service")
.serviceImpl("service.impl")
.controller("controller"))
.strategyConfig(builder -> builder.addInclude("sys_user")
.entityBuilder()
.enableLombok()
.logicDeleteFieldName("deleted")
.versionFieldName("version")
.controllerBuilder()
.enableRestStyle())
.execute();
建议生成完代码之后做两件事:一、删掉自动生成的Controller里那些多余的Swagger注解依赖,避免不必要的jar包引入;二、检查生成的实体类字段类型是不是符合预期,特别是LocalDateTime和BigDecimal这类映射,有时候数据库类型和Java类型转换不对,需要手动调整。
代码生成器的价值在于快速搭框架,而不是一键全自动。生成完的代码必须人工走查一遍,这才是正确姿势。
7. 项目复盘与个人经验分享
模拟项目X从搭建骨架到业务模块落地,前后跑了不到一个月。这套Spring Boot 3 + MyBatis-Plus 3.5.9的组合给我的体感是“快而稳”。快的是CRUD开发速度明显提升,稳的是版本兼容性比预期好很多,没有出现太多需要靠StackOverflow才能解决的疑难杂症。
如果之后有新的项目要从零开始,我依然会优先考虑这套组合。哪怕团队之前用的是MyBatis XML,迁移到MyBatis-Plus也几乎没有成本,因为底层SQL能力没有阉割,只是多了一层单表封装。
最后留几个自己总结的小建议:
- 版本不要追新,稳定优先。MyBatis-Plus 3.5.9是目前比较均衡的版本,没必要为了尝鲜去用刚发布的小版本。
- 条件构造器够用就收,别硬把它当SQL生成器用,复杂查询永远优先选择XML。
- 逻辑删除是好东西,但要注意唯一索引冲突,尤其涉及账号类数据时,提前做好方案设计。
- 分页插件务必配置正确的数据库类型,否则SQL方言对不上,排查成本很高。
- 代码生成器生成的代码不是终点,必须人工走查并补全业务校验和异常处理。
按照我个人经验,这个技术栈最大的收益不是“少写代码”,而是把那些天天重复的基础设施工作压缩到最小,留出精力去处理真正有业务价值的部分。动手去搭一个模拟项目跑一跑,比看再多的文档都有用。
