从SSH连接断开到应用秒起,我在一个遗留项目里完成了从Spring到SpringBoot的迁移。这篇文章不聊理论,就讲这次进化中我踩过的坑、想明白的事,以及那些真正能让你少走弯路的实操细节。无论是还在用XML配Spring的老手,还是刚接触SpringBoot的新人,这篇都能帮你把“进化”这条路看得更透。
1. 这次“进化”的本质,是开发心智的转变
1.1 从Spring到SpringBoot,到底进化了什么
很多人以为SpringBoot只是“简化了配置”或者“内置了Tomcat”,这理解太浅了。真正用过老Spring的人会清楚,Spring的核心价值是IoC容器和AOP,这两样东西到今天依然是SpringBoot的地基。SpringBoot做的不是推翻,而是把Spring当中那些“本来该由框架帮你决定的事”重新收回到框架手里,让你从“装配工”变成“业务设计者”。
我在迁移前那个遗留项目里维护过一套基于Spring Framework 4的老系统,光一个applicationContext.xml就有1500多行,里面密密麻麻的bean定义、aop切面、事务配置、数据源连接池、定时任务声明,看得人头皮发麻。每次新增一个接口,不是先写业务代码,而是先去XML里找有没有现成的bean可以用,没有的话还得自己加<bean>标签,配置各种依赖关系。SpringBoot出现之后,这些全部变成了约定和自动配置,你只需要关心业务本身。
所以说,这次进化不是“换了个启动方式”,而是整个开发模型从“显式装配”进化到了“约定优先”。SpringBoot用一套默认值帮你做了80%的决策,剩下的20%通过配置项和自定义starter来覆盖。理解了这一点,你才能理解为什么SpringBoot会成为现在Java后端的事实标准。
1.2 迁移前的项目画像与迁移目标
我当时的项目是一个典型的“中年系统”:基于Spring Framework 4.3、JDK 8、Maven构建,部署在Tomcat 8上,通过war包发布。系统里有几十个Controller、十来个Service实现类、若干个MyBatis的Mapper接口,外加一套基于Spring Task的定时任务,以及基于ActiveMQ的消息消费者。数据库是MySQL,缓存用了Redis,但缓存逻辑全部手写在ServiceImpl里,没有一个统一抽象。
迁移目标很明确:不改业务逻辑、不换数据库结构、不动前端接口协议。说白了,就是把Spring MVC那套老工程,平移到SpringBoot上,让后续迭代能享受到自动配置、Starter生态和更快的启动速度。这种“不改变系统外在行为,只改变内部实现方式”的迁移,是最考验功底的,因为很多隐藏的老逻辑会在切换过程中突然冒出来,给你一个“惊喜”。
我给自己定了几条硬性标准:
- 业务代码改动量控制在15%以内,超过这个范围就要反思迁移方案是否合理。
- 所有对外接口的URL和参数格式不能有任何变化,避免前端联动修改。
- 迁移完成后,线上接口的响应时间波动不能超过10%,否则要回滚排查。
- 定时任务、消息消费者必须用SpringBoot原生的方式管理起来,不能再依赖外部Tomcat的
web.xml配置。
这些标准明确了“进化”的边界:不是重写,而是换引擎。
2. 核心细节解析:配置进化与自动配置原理
2.1 从XML到JavaConfig,再到自动配置的演进路线
老Spring项目的配置依赖XML,SpringBoot虽然支持XML,但现代写法是JavaConfig和自动配置的组合。这里面有一条清晰的演进脉络,看懂了它你就明白为什么SpringBoot能“跑起来就有默认配置”。
第一阶段是纯XML时代。你在applicationContext.xml里定义数据源、事务管理器、SqlSessionFactory、MapperScannerConfigurer,每一个bean都要手动声明id和class,还经常遇到bean之间循环依赖需要ref来回指。这段时期最大的问题不是写法繁琐,而是配置散落各处,没有统一约束。团队里不同人负责的模块,各自有一套数据库连接的配置方式,换个人接手根本看不懂另一个模块的bean在描述什么。
第二阶段是JavaConfig时代。Spring 3.0之后引入了@Configuration和@Bean,你可以用Java类来定义bean。比如数据源可以这么写:
@Configuration public class DataSourceConfig { @Bean public DataSource dataSource() { DruidDataSource ds = new DruidDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/app"); ds.setUsername("root"); ds.setPassword("123456"); return ds; } }这种写法的好处是类型安全、IDE友好、重构方便,但它依然是“显式声明”。你还是要自己告诉Spring“DataSource长什么样”,只是换了一种表达方式。
第三阶段就是SpringBoot的自动配置。它通过@EnableAutoConfiguration结合spring.factories里的配置类清单,再配合@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,在运行时根据classpath里有没有对应的依赖、容器里有没有用户自定义的bean,自动决定是否装配某一套默认配置。
举个例子:你引入了spring-boot-starter-data-redis依赖,classpath里就有RedisTemplate和LettuceConnectionFactory这些类,自动配置组件检测到这些类存在,并且发现你没有自己定义RedisConnectionFactory,它就会自动创建一个基于默认连接参数的工厂,并在application.yml里读取spring.redis.host、spring.redis.port等属性来填充。整个过程你不用写一行装配代码。
这个机制是SpringBoot最核心的进化点,也是很多面试和实际排查问题绕不开的坎。记住:自动配置不等于不可配置,它只是给了你一套合理的默认值,当你的需求偏离默认时,你依然可以通过自定义bean来覆盖它。这是“约定优先,可覆盖”的设计哲学,也是它和传统Spring最大的区别。
2.2 starter依赖背后的依赖管理魔法
SpringBoot的starter是另一个让人舒服的设计。传统Spring项目里,引入一个技术组件往往要手工拉好几个jar,还得自己处理版本冲突。比如要用Spring MVC,你得找spring-webmvc、spring-core、spring-context、jackson-databind、validation-api等等,版本还得互相兼容,稍不留神就给你整出个NoSuchMethodError。
SpringBoot把这些问题全交给了spring-boot-dependencies这个BOM(Bill of Materials)来管理。你只需要在pom.xml里引入:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>这个parent POM里锁定了所有SpringBoot支持的第三方依赖版本,你再引入任何starter或者底层库时,只要不显式声明version,Maven就会自动采用BOM里那个经过测试的版本。这从根本上解决了两类问题:
- 不同库之间的版本兼容性不再靠开发人员手工试错;
- 升级SpringBoot时,连带升级的依赖版本能保持一个大版本的兼容性。
我这里列一个我在迁移中重点用到的starter对照表,方便你快速评估自己的项目需要引入哪些依赖:
| 原Spring架构中的组件 | 迁移前后的依赖形态 | 说明 |
|---|---|---|
| spring-webmvc + jackson | spring-boot-starter-web | 内置Tomcat、Jackson、Spring MVC自动配置 |
| mybatis + mybatis-spring | mybatis-spring-boot-starter | 自动装配SqlSessionFactory和Mapper扫描 |
| Druid连接池 | druid-spring-boot-starter | 自动识别配置项,并注册StatViewServlet |
| spring-data-redis + jedis | spring-boot-starter-data-redis | 默认使用Lettuce,替换为Jedis需要额外排除依赖 |
| spring-task + quartz | spring-boot-starter-quartz | 简化了SchedulerFactoryBean的配置 |
| javax.validation + hibernate-validator | 自带于starter-web | 参数校验开箱即用 |
这个表对我来说是迁移前做依赖梳理时的一个缩略版,实际项目里可能更多。不过核心思路一样:把原来一个个手动维护的依赖,替换成语义清晰的starter,然后让Maven的依赖管理统一约束版本。
2.3 配置文件的进化:从properties到YAML
老项目里的application.properties其实也能用,但SpringBoot更推荐YAML格式的application.yml,因为它天然支持层级结构,表达能力比properties强不少。举个例子,数据源配置在properties里是:
spring.datasource.url=jdbc:mysql://localhost:3306/app spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver在YAML里等价于:
spring: datasource: url: jdbc:mysql://localhost:3306/app username: root password: "123456" driver-class-name: com.mysql.jdbc.Driver看着差不多,但一旦配置项变多,层级和可读性的差距就体现出来了。尤其像Redis、RabbitMQ、线程池、日志级别这种本身就带有层级属性的配置,YAML的表达更自然,也不容易出现重复前缀的拼写错误。
我在迁移中踩到一个YAML的坑:YAML对字符串的解析有时会自作聪明。比如password写成了123456没问题,但如果你密码恰好是abc:123这种带特殊字符的,就必须用引号括起来。更隐蔽的是,某些配置项如果写成数字开头、或者看起来像时间格式,YAML解析器会把它转成其他类型,导致Spring读取时类型转换失败。我建议配置中涉及非纯数字字符串时统一加引号,这是最省心的做法。
3. 实操过程与核心环节实现:一次完整的迁移流程
3.1 第一步:搭建SpringBoot工程骨架
迁移的第一步不是把老代码复制进来,而是先搭好一个新的SpringBoot工程骨架。我的做法是直接用Spring Initializr生成一个最基础的SpringBoot 2.7.18项目,groupId和artifactId沿用老项目的坐标,然后删掉自动生成的启动类测试目录,只留最基本的包结构。
为什么不直接在老工程里加依赖?因为老工程的pom.xml和目录结构已经被Tomcat、war插件、老的构建流程搞得很复杂,直接在里面改容易引入兼容性隐患。新起骨架的好处是:
- 依赖初始状态干净,方便定位问题;
- SpringBoot的目录结构和规范从一开始就是对的;
- 老代码可以作为资源按需迁入。
生成完骨架后,我把老项目src/main/java下的包目录整体复制到新工程里,再把src/main/resources下除了application.properties之外的文件,比如mybatis的mapper.xml、log4j2.xml、sql初始化脚本等也复制进去。第一轮不要求编译通过,先把结构摆好。
接着修改启动类,让它的@SpringBootApplication能扫描到老代码的所有包路径。这里有个常见误区:启动类所在的包路径如果和老的包结构不一致,SpringBoot默认只会扫描启动类所在包及子包,导致老的Controller、Service全部注册不进去。解决办法是显式指定扫描路径,比如:
@SpringBootApplication(scanBasePackages = {"com.oldproject.app", "com.oldproject.common"}) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }但我不建议一上来就用scanBasePackages硬指定,因为这会掩盖包结构不合理的问题。更稳妥的做法是把老代码的根包尽量调整成和启动类同层次,或者直接让启动类放到老代码根包的上层目录,让默认扫描就能覆盖到。
3.2 第二步:处理数据源与事务配置
数据源是迁移中第一个会“暴雷”的部分。老项目里Druid连接池的配置分散在spring-datasource.xml里,里面有各种init-method、destroy-method、filters、connectionProperties之类的配置。迁到SpringBoot后,我直接把Druid相关配置项放进application.yml:
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://localhost:3306/old_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: "encrypted_password" driver-class-name: com.mysql.cj.jdbc.Driver druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 50这里有个必须注意的点:serverTimezone=Asia/Shanghai不能省,老项目用的是旧版本JDBC驱动,没这个参数MySQL也认,但MySQL 8+的驱动和旧驱动对时区处理的逻辑完全不同,不配的话可能出现8小时的时差,或者直接报Cannot create PoolableConnectionFactory。
事务配置这块,老Spring用的是<tx:advice>加aop切面来声明事务边界。SpringBoot里,我在启动类上直接加了@EnableTransactionManagement,同时在ServiceImpl类或方法上改用@Transactional注解。对于老代码里已经用了@Transactional的部分,基本不用改动;但对于那些把事务声明在XML里的Service,就需要逐个排查,确认哪些方法的旧配置是有rollback-for等特殊要求的,再转化为注解属性。
老项目里有一种比较坑的写法:在事务配置里用了read-only="true"来优化查询方法。迁移时如果漏掉这个属性,可能让一些长事务持有数据库连接的时间变长,导致连接池耗尽。所以查迁移清单时,我特意把XML里所有事务属性都抽了出来,做了一张对照表,逐条核对新注解是否完整覆盖。
3.3 第三步:迁移MyBatis与Mapper层
MyBatis的迁移在SpringBoot里要格外小心。老项目里用的是mybatis-spring的MapperScannerConfigurer,包路径写在XML里。SpringBoot下我用的是mybatis-spring-boot-starter,关键配置项如下:
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.oldproject.app.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl这里mapper-locations一旦配置错,启动时就会报“Invalid bound statement (not found)”的错。我遇到过三种情况:
- mapper.xml文件在resources下的路径和配置不匹配;
- Mapper接口有对应的XML文件,但XML里的namespace写错,跟接口全限定名对不上;
- 新工程打包时把mapper.xml排除出去了,因为Maven默认只打包resources目录下匹配的格式,如果xml放在了
src/main/java下面,会被当作源码不打包。
最省事的定位方式:启动时如果某个Mapper方法报找不到绑定语句,先看控制台里mybatis的mapper-locations实际匹配到了哪些文件,再用mvn clean package后解压war/jar检查xml是否在classes目录里。这三板斧下来基本上都能解决。
另外,老项目很多Mapper接口里用@Param传递多个参数,或者直接用Map传参。迁移时我统统没动,因为MyBatis的运行逻辑不受SpringBoot影响,只是bean装配方式变了。真正需要动的地方是,以前在XML里配置的typeAliasesPackage现在要写进配置,否则实体类用了别名的地方可能会识别不了。
3.4 第四步:REST接口与静态资源处理
Controller这一层的迁移,主要工作量不在注解上,而是在返回值和异常处理上。老项目很多Controller直接返回ModelAndView或者转发到JSP页面,SpringBoot默认不支持JSP内嵌运行,这就需要分情况处理:
- 纯JSON接口:只需确保类上有
@RestController或方法上有@ResponseBody; - 返回页面:如果必须保留JSP,需要额外引入
tomcat-embed-jasper依赖,并设置spring.mvc.view.prefix和suffix; - 静态资源:老代码放在
webapp/static下的js、css、images,SpringBoot默认静态资源路径是classpath:/static/,需要迁移到src/main/resources/static下。
我那个项目里没有JSP,全是前后端完全分离的JSON接口,这块还算省心。但有一个坑:老代码里用了自定义的HandlerInterceptor来做登录态校验,在XML里声明了<mvc:interceptors>。SpringBoot里要改成实现WebMvcConfigurer,注册拦截器:
@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/captcha"); } }这块我一开始忘了迁移,结果启动后所有接口都能直接访问,登录态校验完全失效。幸好有接口权限测试用例兜底,才在一个下午的联调中发现异常。
3.5 第五步:定时任务与异步线程池
老项目里的定时任务是在XML里用<task:scheduled>声明的,每个任务有独立的cron表达式。迁移到SpringBoot很简单:在启动类或单独的配置类上加上@EnableScheduling,然后在任务方法上标注@Scheduled(cron = "0 0 2 * * ?")。但有一个隐藏细节需要留意:定时任务默认是单线程执行的。老项目里如果多个任务本来就在不同线程中跑,迁移后你可能会发现部分任务出现“饿死”或者执行时间重叠导致的互锁问题。
我当时的做法是在配置类里显式声明一个线程池用来跑定时任务:
@Configuration public class SchedulingConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10)); } }这样所有@Scheduled方法都会在这个池子里调度,避免单线程相互阻塞。类似的道理也适用于异步任务。老项目如果没有自定义TaskExecutor,SpringBoot默认的SimpleAsyncTaskExecutor每次都会新建线程,资源消耗大。我建议迁移时顺手定义一个ThreadPoolTaskExecutor,并设置合理的核心线程数、队列容量和拒绝策略,然后把@Async方法使用到这个线程池。这一步不算“必须”,但对系统长期稳定运行非常关键。
4. 常见问题与排查技巧实录
4.1 启动失败:端口占用与上下文路径冲突
老项目部署在Tomcat里,访问URL通常带一层上下文路径,比如http://ip:8080/old_app/api/xxx。SpringBoot默认没有上下文路径,直接监听根路径。为了保证前端代码不用改,我需要在配置里设置:
server: port: 8080 servlet: context-path: /old_app这样接口路径就和老环境完全一致了。如果你没有设置context-path,前端调用的URL会少一层前缀,直接就是404。这个问题在迁移后联调时特别容易一头雾水,我建议第一件事就确认这个配置。
端口冲突是另一个常见启动问题。如果本机或者测试环境已经有服务占用8080端口,启动就会报Port already in use。排查时可以先看是哪个进程占用了端口(Linux下用lsof -i:8080,Windows下用netstat -ano | findstr 8080),确认不是自己人启动的旧服务后,切换到其他端口。
4.2 启动失败:自动配置导致的数据源初始化过早
有一个很隐蔽的问题:如果你的classpath里同时存在多个数据源依赖,比如Druid和HikariCP都在,SpringBoot的自动配置会采用默认的HikariCP。如果你的旧代码在某个@Configuration里自定义了数据源bean,又碰巧那个bean的初始化顺序比较靠前,可能在属性绑定还没完成时就尝试连接数据库,直接报错。
解决思路是不要同时引入多个数据源starter,确定只用一个连接池;如果出于某种原因必须共存,需要在自定义数据源的bean上加@Primary,并指定初始化顺序。我那个项目里就是因为pom.xml里既有Druid的依赖,又带上了spring-boot-starter-jdbc间接引入的HikariCP,导致配置混乱。后来我把Druid作为唯一连接池,所有依赖都集中在Druid的starter上,问题才彻底消失。
4.3 运行时问题:Redis连接的序列化器不一致
老项目里自己封装了一套基于Jackson的Redis序列化方式,SpringBoot自动配置默认用的是JdkSerializationRedisSerializer,这导致两个问题:
- 老代码写入Redis的数据,用新配置读出来时会反序列化失败;
- JdkSerialization存储的二进制数据可读性差,运维排查也不方便。
迁移时我显式定义了RedisTemplate的序列化器:
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; }这个改动让缓存数据以JSON形式存储,跟老系统里已有的JSON格式保持一致。如果你只是用了StringRedisTemplate,那不受影响,因为它的序列化器本来就是String格式。
4.4 慢接口排查:新架构下如何快速定位瓶颈
迁移完成后,我开始比对迁移前后的接口响应时间。如果发现某些接口变慢了,先不要怀疑SpringBoot本身。SpringBoot的自动配置默认会引入性能更好的HTTP容器和连接池,整体不会比老Tomcat差。大多数变慢的原因是:
- 漏掉了原来在XML里的某个连接超时设置,导致连接建立变慢;
- MyBatis的一级缓存或二级缓存配置没有迁移;
- 老代码里用了一个全局的ResponseBodyAdvice,迁移时漏掉了,导致JSON序列化路径变了;
- DataSource的
max-active配置比以前小,遇到并发高峰后线程都在等连接。
排查建议分三步走:先看慢接口的耗时分布在连接池等待、SQL执行、外部调用还是JSON序列化;然后对照组件官方文档检查对应配置是否有缺失;最后用压测工具做一次对比压测,确认迁移前后的容量水位。
这里我还想说一个实用技巧:老代码如果用了Log4j,而SpringBoot默认用的是Logback,迁移时日志输出的格式、级别、滚动策略全都不一样,排查线上问题时会很不适应。建议直接保留Log4j2,引入spring-boot-starter-log4j2并排除Logback,把原来的配置微调成新格式。这样你的日志分析脚本不用改,排查习惯也不用变。
5. 迁移后的性能与稳定性验证
5.1 启动速度与资源占用的对比
迁移完成后我做了一组耗时测试。老Tomcat启动到接口可访问,平均需要35到45秒,期间需要完成web.xml解析、XML配置加载、组件扫描和初始化。SpringBoot内嵌Tomcat后,启动只需要8到11秒,而且不需要单独部署容器,发布包直接从war换成了可执行的jar,运维流程简化了一大截。
内存占用方面,老项目在Tomcat里常驻大概500到600MB的堆内存,SpringBoot内嵌容器后业务量相同的场景下,占用略微降低,主要是因为省掉了一层容器管理器自身的开销。这里要注意一个隐藏点:SpringBoot默认使用的Tomcat是基于NIO的,连接数配置、线程池模型和老版本Tomcat的BIO模型差别很大。如果你老环境里调整过server.xml的maxThreads、acceptCount,这些参数要对应映射到server.tomcat.max-threads和server.tomcat.accept-count配置项,否则遇到高并发时表现会差不少。
5.2 用压测来验收迁移质量
我迁移后做了一轮JMeter压测,目标接口包括一个高频列表查询、一个订单写入接口和一个带Redis缓存的用户信息接口。每个接口分别用200、500、1000并发跑一轮,观察TPS、响应时间P95/P99和错误率。
结果很有意思:列表查询接口的TPS比老系统提升了约12%,原因是SpringBoot自动配置帮我们换上了更合理的连接池参数和更高效的Jackson序列化配置;订单写入接口基本持平,说明瓶颈确实在数据库事务层面,而不是框架;带Redis缓存的接口性能波动明显变小,这是因为Lettuce客户端本身的线程模型就比老项目里手写的Jedis实现更稳定。
压测过程中我还发现一个配置遗漏:老系统里用了Druid的监控页面,迁移后默认是关闭的。我把Druid内置的StatViewServlet注册为一个bean,并设置好访问白名单,这样运维和开发可以继续通过监控页面看SQL执行统计、慢SQL和连接池状态。这一步不要省,尤其是生产环境,没有监控面板你是很难判断新环境下连接池行为是否健康的。
6. 扩展思考:这个进化的路还能怎么走
SpringBoot不是终点,它只是一个新的起点。完成迁移后,项目在几个方向上继续走会顺畅很多:
第一,模块化。老项目是一个典型的单体应用,SpringBoot虽然不强制你拆分,但它的starter机制和自动配置让模块边界变得更好定义。你可以把公共能力,比如通用返回结构、异常体系、权限校验、API日志,抽成公司内部的starter,然后在各个微服务里统一引入。这一步在Spring时代很难做利索,因为手动管理几十个jar的版本本身就是噩梦,SpringBoot的BOM帮你解决了版本地狱后,这事就自然了。
第二,云原生部署。SpringBoot可执行jar配合Docker天然合适,我迁移完后顺手写了一个多阶段构建的Dockerfile,镜像体积比原来的Tomcat镜像小了30%以上,启动时间也更快。配合Kubernetes部署后,发布流程可以做到滚动更新和快速回滚,这是老部署方式很难实现的。
第三,可观测性。SpringBoot内置了Actuator端点,能暴露健康检查、指标、线程转储、堆内存使用等运行数据。老项目里要拿到这些信息,还得额外引入一套监控SDK并做大量配置。SpringBoot迁移完成后,接监控系统变得很轻量,只需要在配置里开启对应端点,再接入现有的Prometheus或类似采集体系就行。
我个人的经验是,不要把“进化”想成一次性的大工程。它更像是先把地基换掉,然后你才有的选,选微服务、选云部署、选更完善的监控体系。如果还留在纯Spring的配置泥潭里,这些后续动作的成本都会成倍增加。
最后分享一个我在实际迁移中一直坚持的习惯:每一项配置从一个体系换到另一个体系时,都顺手写一行注释,说明这行配置对应老配置里的哪个文件哪一段。这个习惯帮我省了无数次返工,也让后续接手的人不用对着旧工程反向翻译。进化的路上,比改代码更重要的,是让这套新体系能被团队里的人真正理解和维护。