1. 从SSM到SpringBoot3:为什么我建议你直接跨过传统配置
做Java后端这几年,SSM(Spring+SpringMVC+MyBatis)这套组合几乎是每个入门者绕不开的路。但说实话,我见过太多人在XML配置、web.xml、各种jar包冲突里消耗掉大量热情,最后连业务都没怎么写就放弃了。而SpringBoot3配合MyBatis-Plus的出现,直接把这条路从“翻山越岭”变成了“走平路”。如果你现在还在纠结要不要从SSM框架转过来,我的建议很直接:别纠结,直接学SpringBoot3+MyBatis-Plus这套组合,尤其是用SpringBoot3的新特性去重构你手头的SSM项目,那种爽感只有亲自试过才知道。
这篇文章就围绕一个具体的实战课题——ssm-day07,也就是SpringBoot3、MyBatis-Plus、SpringBoot实战中非常典型的一次项目迭代记录,把我实际搭建、迁移、踩坑、优化的全过程拆开讲。内容包括:为什么SpringBoot3比老SSM更适合做新项目、MyBatis-Plus到底解决了什么问题、SpringBoot3下的日志配置(尤其是logback-spring.xml的坑)、批量操作的性能对比,以及Spring Data JPA和MyBatis-Plus到底怎么选。无论你是刚学完SSM想升级,还是已经用了多年SpringBoot2想看看3的变化,这篇文章都值得你花点时间读完。
2. 内容整体设计与思路拆解:SSM老项目的困境与SpringBoot3的破局
2.1 传统SSM框架到底“重”在哪里
先回顾一下传统SSM的搭建过程。Spring要配置IOC容器、SpringMVC要配置DispatcherServlet、MyBatis要配置SqlSessionFactory和Mapper扫描,这三者之间还要通过Spring的配置文件互相引用。我记得早期搭一个空项目,光applicationContext.xml和spring-mvc.xml就能写上百行,还要处理web.xml里的监听器、过滤器、DispatcherServlet映射。最要命的是版本兼容问题:Spring 4配MyBatis 3、Spring 5配MyBatis 3.5,稍微一个版本对不上,启动时就是各种NoSuchBeanDefinitionException或者ClassNotFoundException。
说白了,那时候的时间都花在“让框架跑起来”上,而不是“让业务跑起来”。我们团队曾经接手一个老SSM项目,光是升级Spring版本就折腾了两天,最后因为某个第三方依赖的传递依赖冲突,只能选择回滚。这种经历多了,你自然会想问:有没有一种方式,让我把精力放在写Service和Mapper上,而不是跟配置文件搏斗?
2.2 SpringBoot3带来的核心变化
SpringBoot3最大的变化是基于Jakarta EE 9+,原来的javax.*命名空间换成了jakarta.*。这句话听起来简单,但它意味着所有基于SpringBoot3的项目,如果你的旧代码用了javax.servlet、javax.annotation这些包,都需要做包名迁移。我一开始也觉得这是个麻烦事,但实际改起来比想象中简单,因为IDE的批量替换功能可以一键搞定,真正需要手工处理的往往只有自定义注解和极少数的反射代码。
除了包名变化,SpringBoot3还内置了基于GraalVM的原生镜像支持,虽然我没在生产环境大规模使用,但本地测试过,启动时间确实能从几秒降到几百毫秒。另外,SpringBoot3要求Java 17+,这正好逼着我们用上更现代的Java特性,比如record、switch表达式、文本块等。我用record重写了原来的DTO类,代码量直接减少三分之一。
回到实战场景,SpringBoot3最直观的优势是“约定优于配置”。你不需要再写web.xml,不需要配置Spring容器的扫描路径,不需要处理视图解析器,只需要一个带有@SpringBootApplication注解的主类,然后mvn spring-boot:run就起来了。我第一次用SpringBoot3重建SSM项目时,整个搭建过程从原来的半天压缩到十分钟,这种效率提升是肉眼可见的。
2.3 MyBatis-Plus在SSM升级中扮演的角色
SSM里的“M”是MyBatis,而MyBatis-Plus是MyBatis的增强工具。它没有改变MyBatis的核心执行逻辑,只是在通用Mapper、通用Service、分页插件、条件构造器、代码生成器这几个方面做了大量自动化处理。很多人觉得MyBatis-Plus是“换汤不换药”,但实际用下来差别很大。
传统MyBatis,你每写一个对单表的CRUD,就要在Mapper接口里声明一个方法,然后在XML里写一条SQL。如果是几十张表,那就是几十个重复的insert、delete、update、select语句。MyBatis-Plus的BaseMapper接口内置了insert、deleteById、updateById、selectById、selectList等一堆通用方法,你只要继承它,不用写任何SQL就能完成80%的基础操作。
举一个我在迁移中的实例:原来的SSM项目里有一个UserMapper,里面定义了一个findUserByCondition方法,XML里写了十几行动态SQL。迁移到MyBatis-Plus后,我直接用LambdaQueryWrapper,代码变成了这样:
List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>() .eq(User::getStatus, 1) .like(StringUtils.hasText(name), User::getName, name) .orderByDesc(User::getCreateTime));没有XML,没有注解SQL,每行代码都能看明白在干什么。特别是LambdaQueryWrapper这种写法,基于方法引用来获取列名,避免了硬编码字符串字段名导致的拼写错误。我把这个逻辑讲给团队里的新人听的时候打了个比方:你原来写SQL像是手写信封地址,现在写LambdaQueryWrapper像是用通讯录选择联系人,虽然底层还是寄信,但你不用再管邮编、街道名这些细节了。
3. 核心细节解析与实操要点:SpringBoot3+MyBatis-Plus的关键配置
3.1 创建SpringBoot3工程:从骨架开始
SpringBoot3项目推荐用Spring Initializr创建,你可以直接在 start.spring.io 网站上生成,也可以使用IDEA内置的Spring Initializr。建议选择Java 17或更高版本,依赖项选择Spring Web、MyBatis-Plus Framework(这个需要手动添加坐标,因为MyBatis-Plus没有在Initializr的正式列表里,你可以在生成项目后往pom.xml里加)、MySQL Driver、Lombok。
这里有个细节要特别注意:SpringBoot3对应MyBatis-Plus的版本必须用mybatis-plus-spring-boot3-starter,而不是之前SpringBoot2时代的mybatis-plus-boot-starter。我第一次踩这个坑的时候,加入了旧版starter,结果项目启动直接报UnsupportedClassVersionError,原因就是新老starter依赖的Spring Boot版本不兼容。现在MyBatis-Plus官方提供了专门的SpringBoot3适配包,坐标是:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency>pom.xml里还需要注意不要漏掉MySQL驱动,我更喜欢用com.mysql:mysql-connector-j,这是MySQL官方新的驱动包名,老的是mysql:mysql-connector-java。SpringBoot3里如果沿用老的groupId,仍然能引入,但官方已经推荐用新坐标。另外,建议配置Lombok,因为MyBatis-Plus的实体类配合Lombok的@Data注解能省掉大量getter/setter。
3.2 数据源与MyBatis-Plus配置:不写XML的快乐
在application.yml里,我习惯这样配置:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm_day07?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 mybatis-plus: global-config: db-config: id-type: auto logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true这个配置里有几个关键点。
serverTimezone=Asia/Shanghai是必须的,否则MySQL驱动8.x版本会报时间区错误。allowPublicKeyRetrieval=true也是MySQL8连接时需要加的,不然在某些环境下会提示Public Key Retrieval is not allowed。
MyBatis-Plus的id-type: auto表示主键使用数据库自增策略,前提是你的表主键是AUTO_INCREMENT。logic-delete-field配置了逻辑删除字段,这样你调用deleteById时,MyBatis-Plus会自动把deleted字段从0更新为1,而不是真正删除记录。这个功能对于保留数据审计痕迹非常有用,但要注意,如果你以前在XML里写的是DELETE FROM user WHERE id=#{id}这样物理删除的SQL,统一改成MyBatis-Plus的通用方法后才能触发逻辑删除。
map-underscore-to-camel-case默认就是true,配合实体类字段驼峰命名,数据库字段下划线命名,可以自动映射。比如数据库user_name自动对应Java字段userName,不需要额外注解。这个默认行为已经让我少写了很多@TableField注解。
3.3 实体类与Mapper接口:用代码生成器一劳永逸
手写实体类很容易出错,尤其字段多的时候。MyBatis-Plus提供了AutoGenerator代码生成器,虽然配置稍微繁琐,但确实省力。我也见过有人直接用IDEA的数据库工具生成实体,但我觉得MyBatis-Plus的生成器更符合项目规范,它会自动生成实体类、Mapper接口、Service接口、ServiceImpl实现类,还能生成Controller(不过我不太建议生成Controller,因为业务差异太大)。
我这里展示一个最简单的实体类和Mapper写法:
@Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private String email; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; @TableLogic private Integer deleted; }public interface UserMapper extends BaseMapper<User> { }就这么简单。@TableName指定表名,@TableId指定主键生成策略,@TableLogic标识逻辑删除字段。BaseMapper<User>里已经提供了各种单表方法。你可能会问:那多表联查怎么办?答案是:多表联查仍然需要自己写SQL,MyBatis-Plus没有放弃XML,你依然可以在resources/mapper下放UserMapper.xml,在Mapper接口里声明方法,在XML里写复杂的JOIN语句。MyBatis-Plus不是让你完全不写SQL,而是让你不写那些重复度极高的单表SQL。
3.4 条件构造器:LambdaQueryWrapper和LambdaUpdateWrapper的正确用法
这是MyBatis-Plus最常用也最容易用错的地方。我可以负责任地说,只要掌握了LambdaQueryWrapper,至少能省掉一半的重复SQL。
LambdaQueryWrapper的核心是链式调用,主要用于查询条件拼接。看一个实际需求:分页查询用户列表,条件包括姓名模糊匹配、状态精确匹配、按创建时间倒序。使用方式如下:
Page<User> page = new Page<>(current, size); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(name), User::getName, name) .eq(User::getStatus, status) .orderByDesc(User::getCreateTime); userMapper.selectPage(page, wrapper);注意like方法的第一个参数是boolean condition,如果condition为false,该条件不会拼接到SQL里。这样你就不用每次手写if (name != null && !name.isEmpty())了。这种写法在我们从SSM迁移时是一次巨大的体验升级,原来Service里到处都是if else判断条件,现在一行链式调用就搞定了。
LambdaUpdateWrapper则用于更新操作,比如批量更新状态:
userMapper.update(null, new LambdaUpdateWrapper<User>() .eq(User::getStatus, 0) .set(User::getStatus, 1));这里第一参数传入null表示不使用实体对象赋值,所有更新列都在set方法里指定。使用这种写法时一定要小心,如果set里写错了字段名,虽然编译期不会报错(因为方法引用是类型安全的),但运行时会给你更新错列,所以建议先把字段名对照数据库检查一遍。
我个人的一个实操建议是:查询条件特别简单时直接用QueryWrapper也行,但既然用了MyBatis-Plus,就从第一天开始坚持使用LambdaQueryWrapper。因为方法引用会在编译期检查字段名是否存在,能避免把userName错写成username这种低级问题。我们之前在SSM项目里就真出现过一次姓名字段拼写错误,结果SQL执行报错,线上临时修复,教训惨痛。
4. 实操过程与核心环节实现:SpringBoot3日志配置与批量数据操作
4.1 logback-spring.xml在SpringBoot3中的配置要点
SpringBoot3默认使用Logback作为日志框架,但你直接在application.yml里设置logging.level虽然简单,却无法实现按天滚动、日志文件大小限制、不同环境日志路径隔离这些需求。所以实战项目基本都会引入logback-spring.xml。
这个文件名值得注意,它叫logback-spring.xml而不是logback.xml。区别在于,logback-spring.xml支持SpringBoot的profile特性,你可以在同一个文件里通过<springProfile>标签给dev、prod环境配置不同的日志级别和输出策略。如果用logback.xml,它就只是一个普通的Logback配置,无法读取Spring的配置属性,更不能根据环境切换。
我分享一份比较标准的logback-spring.xml配置骨架,这个配置我在多个SpringBoot3项目里都用过,实测稳定:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 日志存放路径:可以通过application.yml里的log.path属性覆盖 --> <springProperty scope="context" name="log.path" source="log.path" defaultValue="./logs"/> <!-- 控制台输出 --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 文件输出:按天滚动,保留30天 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/app.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 错误日志单独文件 --> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${log.path}/error.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{50} - %msg%n</pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 开发环境:控制台输出DEBUG --> <springProfile name="dev"> <root level="DEBUG"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root> </springProfile> <!-- 生产环境:只输出INFO以上并写到文件 --> <springProfile name="prod"> <root level="INFO"> <appender-ref ref="FILE"/> <appender-ref ref="ERROR_FILE"/> </root> </springProfile> </configuration>这个配置里要注意几个点。
TimeBasedRollingPolicy是时间滚动策略,fileNamePattern里的%d{yyyy-MM-dd}决定了按天切分。如果你想按大小切分,比如每个日志文件不超过200MB,就需要用SizeAndTimeBasedRollingPolicy,并设置maxFileSize。
${log.path}这个占位符从application.yml里读取,你可以这样定义:
log: path: ./logs如果你的项目还有log4j2的需求,SpringBoot3也是支持的,你需要在pom.xml里排除spring-boot-starter-logging,然后引入spring-boot-starter-log4j2。我团队里有一个项目因为需要异步日志和按级别分离,就用了Log4j2。但绝大多数SpringBoot3项目用Logback就足够了,不必盲目追随热词里的log4j2。
4.2 MyBatis-Plus批量的正确打开方式
“Java MyBatis MyBatis-Plus批量”这个词在热搜里出现频率很高,说明很多人对批量操作有困惑。传统MyBatis批量插入的方式是写一个<foreach>标签在XML里循环拼接VALUES,这种方式在数据量小的时候没问题,但一旦达到几百条,生成的SQL字符串会特别长,数据库解析SQL的压力也大,甚至可能触发MySQL的max_allowed_packet限制。
MyBatis-Plus提供了一个IService接口,里面的saveBatch方法可以批量插入,但它的实现方式默认其实是循环调用单条insert,只不过通过SqlSession的批处理模式优化了。注意,默认的saveBatch并不保证是一个SQL插入所有数据,而是分批提交。如果你想真正用一条SQL插入大量数据,还是得自己写在XML里,用<foreach>或者用insert into ... values ... , ...这种方式。
我的建议是:100条以内的数据,直接用saveBatch,省心;1000条以上的数据,建议自己在Mapper XML里写<foreach>,并且控制每批大小在500条左右。举个实际的例子:
List<User> userList = new ArrayList<>(); // 填充数据…… userService.saveBatch(userList, 500);saveBatch(collection, batchSize)的第二个参数是每批提交的数量。底层原理是通过DefaultSqlSession的ExecutorType.BATCH模式,把多个INSERT语句缓存在会话中,最后统一提交。这样做的好处是减少网络往返和事务提交次数。但如果你的批量操作同时包含查询和更新,要注意批处理模式下,刷新时机可能会导致数据不一致,所以建议批处理事务中不要穿插查询。
再聊聊MyBatis-Plus的逻辑删除和批量更新组合。之前说过LambdaUpdateWrapper可以做更新,但批量物理删除则建议用deleteBatchIds:
userMapper.deleteBatchIds(Arrays.asList(1L, 2L, 3L));这句话执行后,MyBatis-Plus会自动把每条记录的deleted字段更新为1(因为配置了逻辑删除),而不是执行DELETE。如果你在控制台看到日志打印的是UPDATE user SET deleted=1 WHERE id IN (?),那就说明逻辑删除生效了。
4.3 分页插件:MyBatis-Plus的良心配置
分页查询是所有后台管理系统的基础功能。SSM时代,我们用PageHelper分页插件,也能实现分页,但MyBatis-Plus的PaginationInnerInterceptor集成更自然。配置方式很简单,新建一个MybatisPlusConfig配置类:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个拦截器的作用是拦截你传给selectPage的SQL,自动拼接LIMIT ?,?。它的核心原理是通过MyBatis的插件机制,在执行SQL前重写SQL语句,加入物理分页参数。不配置这个拦截器,selectPage方法打印出来的SQL还是全量查询,然后内存分页,那样数据量一大就会内存溢出。
使用分页时,Page<User>对象会承载分页结果和总数信息:
Page<User> page = new Page<>(1, 10); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); page = userMapper.selectPage(page, wrapper); System.out.println("总记录数:" + page.getTotal()); System.out.println("总页数:" + page.getPages()); System.out.println("当前页数据:" + page.getRecords());这里有个隐藏细节:selectPage方法会执行一条SELECT COUNT(*)的总数查询,再执行分页数据查询。如果表数据量很大,COUNT查询可能成为瓶颈。MyBatis-Plus提供了一个优化选项optimizeJoinOfCountSql,默认是true,它会尝试把LEFT JOIN优化为不参与COUNT统计,减少无谓的关联计算。如果你的分页查询是多表联查,这个优化效果会很明显。
5. 常见问题与排查技巧实录:那些导致项目起不来的坑
5.1 SpringBoot3启动报错:javax与jakarta的冲突
升级到SpringBoot3之后,最经典的问题就是引入老依赖。比如某些第三方组件仍然用javax.annotation.Resource,而SpringBoot3的容器只认jakarta.annotation.Resource。如果项目引入的某个jar包依赖于旧版javax.annotation-api,就会出现NoClassDefFoundError: javax/annotation/Resource。排查方式很简单,看mvn dependency:tree里有没有带javax的传递依赖,有就通过exclusion排除掉。
5.2 MyBatis-Plus 逻辑删除配置后,查询结果总感觉少了数据
我有一个真实经历:给某张表加了@TableLogic注解后,执行selectList发现结果集比表里的数据少了几条。起初以为代码有问题,排查后发现,那些少的记录其实都是之前手动在数据库里把deleted置为1的脏数据。逻辑删除生效后,MyBatis-Plus的selectList会自动带上WHERE deleted=0条件,所以历史脏数据自然查询不出来了。遇到这种情况,不要怀疑代码bug,先去确认deleted字段的数据是不是有问题。
5.3 分页插件不生效:SQL还是全查
如果selectPage返回的records包含了所有数据,而total也不对,那90%是因为没有配置PaginationInnerInterceptor。还有人会配置了拦截器,但顺序不对。MybatisPlusInterceptor里可以加多个InnerInterceptor,顺序有讲究:如果同时使用多租户插件和分页插件,要把多租户插件放在前面,分页插件放最后,否则SQL拦截顺序可能出错。可以参考官方文档的插件顺序说明,我一般只配置分页和乐观锁插件,顺序是:乐观锁插件在分页前面。
5.4 logback-spring.xml 中的 ${log.path} 不生效
这个问题的根源在于你没有配置springProperty,或者source写错了。在logback-spring.xml里使用Spring配置属性必须声明:
<springProperty scope="context" name="log.path" source="log.path" defaultValue="./logs"/>这里source对应application.yml里的log.path路径,而不是直接写${log.path:./logs}这一套。因为Logback本身不认识Spring的application.yml配置项,必须通过springProperty搭桥。
5.5 实体类字段为is_deleted这类下划线开头的映射问题
如果你表里有个字段叫is_deleted,Java属性写成isDeleted,MyBatis-Plus会自动映射为is_deleted吗?答案是默认可以,因为map-underscore-to-camel-case=true会将驼峰转下划线,isDeleted会转成is_deleted。但要注意Lombok的@Data生成getter时,对于Boolean类型的isDeleted属性,生成的getter是getIsDeleted而不是isIsDeleted,这会导致部分ORM在取属性值的时候出现问题。我建议逻辑删除字段就用Integer deleted,不要用Boolean。
5.6 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
SpringBoot3启动报javax.*相关错误 | 引入了基于javax的旧第三方包 | 使用mvn dependency:tree排查并排除或升级依赖 |
MyBatis-Plus的selectPage没有分页 | 未配置PaginationInnerInterceptor | 添加MybatisPlusInterceptor并注册分页插件 |
insert后主键没有回填 | 实体主键未加@TableId(type = IdType.AUTO) | 确认主键字段配置正确,数据库主键为自增 |
| 逻辑删除后查询不出数据 | deleted字段有脏数据 | 清洗数据,或临时用自定义SQL查询确认 |
| 日志文件不滚动 | fileNamePattern中缺少日期占位符 | 修改fileNamePattern为app.%d{yyyy-MM-dd}.log |
| 批量插入报Mysql packet过大 | 一次插入数据量过大 | 分批插入,建议每批500条左右 |
6. 从SSM到SpringBoot3的项目迁移实录与个人心得
6.1 迁移步骤总结
如果你手上正好有一个SSM老项目要升级到SpringBoot3+MyBatis-Plus,我建议按这样的顺序操作:
第一步,先建一个全新的SpringBoot3工程,确认基础依赖能跑通。第二步,把原项目的pom.xml里业务依赖一个个迁移,每迁移一个就启动一次,确认没有冲突。重点注意旧版MyBatis、PageHelper、Druid等依赖的替换。第三步,把web.xml、applicationContext.xml、spring-mvc.xml里的Bean扫描、拦截器、视图解析器配置,逐步替换成SpringBoot的Java Config或注解方式。第四步,把MyBatis的Mapper接口和XML文件复制过来,先把BaseMapper继承上,能用通用方法的就删掉XML里的重复SQL,无法替代的复杂SQL保留XML。第五步,配置MyBatis-Plus的插件和通用配置,包括逻辑删除、分页、字段自动填充等。第六步,解决日志框架的问题,统一为logback-spring.xml,按环境切换输出级别。第七步,进行全量回归测试,重点测试原来依赖SpringMVC的拦截器、参数校验、异常处理等逻辑是否正常。
我自己迁移一个用户模块差不多花了一下午,大部分时间消耗在旧代码里那些花式注入和自定义拦截器上。等模块迁移完,再看那些代码,明显清爽很多。
6.2 Spring Data JPA和MyBatis-Plus怎么选
这个话题被问的频率很高,尤其是在技术选型时。我的观点是:如果你的项目以单表CRUD为主,领域模型清晰,希望开发效率最大化,用Spring Data JPA没问题。但如果你需要精细控制SQL,尤其是复杂的多表关联、报表统计、动态条件拼装,MyBatis-Plus更顺手。Spring Data JPA在实体关系映射上确实强大,但它的懒加载、N+1问题需要一些经验才能优雅处理。而MyBatis-Plus天然是SQL导向,写复杂查询更直接,排查SQL语句也更符合国内开发者的直觉。
从团队协作角度讲,新人对MyBatis-Plus的接受门槛明显更低,因为只要会写SQL,就能快速上手LambdaQueryWrapper,而JPA的@EntityGraph、Specification、QueryDSL等概念有一定学习曲线。当然,这不是说JPA不好,只是两者的思维模式不同。如果你是一个从SSM项目切换过来的团队,选MyBatis-Plus的迁移成本几乎为零。
6.3 关于SpringBoot3日志配置的一点实战心得
我在一个SpringBoot3项目里曾经花了一整个下午排查日志文件不滚动的问题,最后发现根因是fileNamePattern写成了app.log.%d{yyyy-MM-dd},而logback强制要求<file>里的文件名不能包含日期变量,但fileNamePattern里的也强调必须从“基础文件名”派生。当时我的配置里<file>写的是app.log,fileNamePattern写的是app.log.%d{yyyy-MM-dd},理论上应该没错,却一直不切分。后来排查发现是编码问题,配置文件里有一个不可见字符。这个经历让我学到一个教训:配置文件出了问题,优先检查文件编码、缩进和不可见字符,很多时候不是语法不对,而是肉眼看不到的字符在捣乱。
6.4 个人体会与后续扩展
从ssm-day07这个课题往回看,真正让一个后端项目“顺滑可控”的,并不是用了多牛的框架,而是框架把那些琐碎的配置和重复劳动接管了。SpringBoot3+MyBatis-Plus就是这样一对组合,它们不解决业务复杂度,但能把你从配置泥潭和重复SQL里解放出来,让你把时间花在真正需要思考的业务逻辑上。
如果你正在学SSM,我建议你学完基础概念后,可以直接跳到SpringBoot3实战,把SSM当作理解底层原理的辅助工具,而不是日常开发主框架。如果你在工作中维护老SSM项目,也可以挑一个小模块先做迁移试点,感受一下SpringBoot3的开发效率,再逐步推进。MyBatis-Plus的好处在于它并不会让你彻底抛弃SQL,它还保留着XML扩展能力,所以老项目的复杂查询很容易迁移过来,不用担心推倒重来。
最后再分享一个小技巧:SpringBoot3项目里,建议把spring.datasource.url、username、password这些环境相关配置放到application-prod.yml里,利用spring.profiles.active=prod切换。同时,在logback-spring.xml里配合<springProfile name="prod">,让生产环境的日志级别和输出文件和开发环境完全隔离。这套组合就是很多生产项目稳定运行的基石,也是我认为ssm-day07这个课题最值得沉淀下来的实战经验。