如果你今天打开项目的pom.xml,看到 Spring Boot 版本停在3.2.x,然后顺手把 Apache Shiro 从1.13.0升到2.0.1,恭喜你,你即将进入一个从javax.servlet到jakarta.servlet的全新世界。我第一次升这个版本的时候,项目启动第一秒就抛了ClassNotFoundException: javax.servlet.Filter,当时第一反应是“这包我明明导了”,结果发现根本不是包缺失的问题,而是整个 Servlet 命名空间都换了血。那两天我几乎把公司项目的依赖树翻了个底朝天,才把整套过滤器链重新捋顺。
这篇文章就围绕 Spring Boot 3.2 集成 Shiro 2.0.1 这条主线,讲清楚javax.servlet到jakarta.servlet迁移过程中会遇到哪些问题、为什么会发生这些问题、每一步怎么排查和解决。内容不绕弯子,直接以我实际踩过的坑为主线,适合正在做 Spring Boot 3 迁移、或者打算升级 Shiro 的团队参考。如果你只是把 Spring Boot 从 2.x 升到 3.x,但 Shiro 还停在 1.x,那这篇文章更要看完,因为你大概率会在上线前夜被NoClassDefFoundError折腾到怀疑人生。
1. 根源在名字:一场从 javax.servlet 到 jakarta.servlet 的改名运动
1.1 Java EE 更名史:为什么代码里会同时存在 javax 和 jakarta
很多人第一次看到jakarta.servlet这个包名时会愣住,心想“是不是写错了”。没写错,这是真实存在的命名空间,背后是一段 Java 企业级技术栈的改名历史。
早期 Java 企业级开发基于 Java EE,命名空间统一是javax.*。Oracle 后来把 Java EE 捐给了 Eclipse 基金会,基金会不能继续使用 Java EE 这个商标,于是项目更名为 Jakarta EE。改名不可怕,可怕的是 Jakarta EE 9 开始规定:所有原本在javax.*下的 API 包,必须迁移到jakarta.*下。这就是为什么今天你看到的javax.servlet.http.HttpServletRequest变成了jakarta.servlet.http.HttpServletRequest。
这里面有一个关键区别:javax.servlet和jakarta.servlet是两套完全不同的 API,底层字节码层面就不兼容。哪怕你只是把一个HttpServletRequest类型搞混,都会直接抛ClassCastException或者NoClassDefFoundError。
我经常打一个比方:这就好比老小区改了个新名字,身份证、户口本、水电煤气账单全部要跟着换。你本人还是那个人,但所有和外部系统对接的凭证都得换一版。Spring Boot 3.x 就是那个换完户口本的新小区,而 Shiro 1.x 还拿着老身份证,进不了门。
1.2 Spring Boot 3.2 为什么必须切换 Servlet API 版本
Spring Boot 3.0 开始,官方将基线升级到 Jakarta EE 9+,这意味着它内嵌的 Tomcat 版本已经是 10.x,而 Tomcat 10 之后已经彻底移除javax.servlet支持,只提供jakarta.servlet实现。
Spring Boot 3.2 则是在这个基础上进一步稳定的版本,要求 JDK 17+。也就是说,不管你想不想,只要你的项目跑在 Spring Boot 3.2 上,Servlet 容器层面的 API 就已经是jakarta.*了。
这里有个很容易踩的隐性坑:IDE 自动补全。当你输入HttpServletRequest时,IDE 可能会同时提示javax.servlet.http.HttpServletRequest和jakarta.servlet.http.HttpServletRequest。如果在 Spring Boot 3.2 项目里误选了javax版本,编译期大概率不会报错,因为项目里可能还有别的依赖间接引用了javax.servlet-api,但运行时就会出现各种诡异问题。这个问题在 Shiro 集成时会尤其明显,因为 Shiro 1.x 的过滤器全部基于javax.servlet编译,放进 Tomcat 10 里根本没法用。
1.3 Shiro 2.0.1:一个不得不出现的大版本
Apache Shiro 1.x 是很成熟的身份认证与授权框架,在国内企业项目里使用率很高,尤其是那些基于 Spring Boot 2.x 搭建的后台管理系统、企业项目进度跟踪系统、工时管理系统,几乎人手一个 Shiro。
问题在于,Shiro 1.x 是从javax.servlet时代走过来的,所有 servlet 相关的过滤器、Filter 链、会话管理等都是基于旧 API。Spring Boot 3 已经切到jakarta了,Shiro 要么跟着切,要么就别想兼容新容器。
于是 Shiro 2.0.0 出现了,这是 Apache Shiro 全面支持 Jakarta EE 9+ 的首个大版本,2.0.1 则是 2.0 系列趋于稳定的维护版本。我们项目用的就是 Shiro 2.0.1,配合 Spring Boot 3.2,整体跑下来已经比较顺了。
另外,Shiro 1.x 时代的安全漏洞问题也很让人头疼。安全扫描工具一跑,shiro-core的 CVE 列表能列出好几条,尤其是在 rememberMe 相关的 Java 序列化问题上。升级到 2.0.1 之后,虽然不能说绝对安全,但至少能清掉一批 1.x 时代遗留的高风险告警。从安全合规角度来说,这也是一个重要的升级理由。
2. 依赖层面的清扫:先让 Maven 告诉我们谁还抱着 javax 不放
2.1 一份能直接在 Spring Boot 3.2 里跑的 Shiro 2.0.1 依赖清单
在准备开始写配置类之前,先把pom.xml理清楚。我推荐的最小依赖清单是这样的:
<properties> <java.version>17</java.version> <spring-boot.version>3.2.5</spring-boot.version> <shiro.version>2.0.1</shiro.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Shiro 核心 + Spring Boot Web 集成 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>${shiro.version}</version> </dependency> <!-- Shiro 与 Spring AOP 集成,用于注解权限 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> </dependencies>这里有几个细节值得注意:
第一,shiro-spring-boot-web-starter是 Web 项目专用的 starter,它会把 Shiro 的自动配置带进来。如果你的项目是纯后端 API,建议直接用这个;如果只是普通业务项目,没有 Web 层,那才考虑shiro-spring-boot-starter。
第二,shiro-spring提供了 Spring 集成相关的类和 AOP 支持。虽然 starter 内部可能已经传递依赖了shiro-spring,但我习惯在pom.xml里显式声明,避免后续版本升级时丢依赖。
第三,java.version必须 17+,这是 Spring Boot 3.x 的硬性要求,跟 Shiro 无关。
2.2 用 mvn dependency:tree 揪出所有 javax.servlet 依赖
引入 Shiro 2.0.1 之后,项目如果还能启动,说明你的依赖树已经基本健康。但很多人的项目里还有别的老依赖,比如老版本的 Ehcache、Hibernate Validator、某些第三方 starter,它们可能会传递引入javax.servlet-api。
这种依赖通常不会直接编译报错,因为你的代码用的是jakarta,但一旦某个框架内部用反射加载了javax.servlet.Filter,就会在运行时炸出来。而且这种错误往往特别诡异,报错点可能在 Spring 容器刷新阶段,也可能在你访问某个接口时。
排查方法很简单,直接在项目根目录执行:
mvn dependency:tree -Dincludes=javax.servlet这条命令会列出所有依赖树中带javax.servlet的坐标。我实际遇到的情况是一个老版本的shiro-redis-spring-boot-starter间接引入了javax.servlet-api,而它的代码又是基于javax.servlet.http.HttpSession编译的,导致启动时直接进入错误分支。
处理方式一般有两种:升级这个第三方依赖到支持jakarta的版本;或者如果短期内升不了,就在自己的pom.xml里对它做排除:
<dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis-spring-boot-starter</artifactId> <version>3.3.1</version> <exclusions> <exclusion> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </exclusion> </exclusions> </dependency>但这里要提醒你:排除依赖只是治标,如果那个库内部真的用了javax.servlet的类,排除之后会变成NoClassDefFoundError。所以最好的方案还是找一个已经适配jakarta的版本。
2.3 不只是 servlet:其他 javax 包也一起迁移了
这次迁移不只是javax.servlet改成jakarta.servlet,其他 Java EE API 也一样:
javax.validation→jakarta.validationjavax.annotation→jakarta.annotationjavax.persistence→jakarta.persistencejavax.transaction→jakarta.transaction
其中最容易让老项目翻车的,是javax.annotation.PostConstruct。很多人在自定义 Filter、监听器、工具类里用过@PostConstruct,升级之后如果没注意 import,启动时 Spring 容器可能根本不会调用这个初始化方法,导致一些静态数据没加载。
另外,JDK 11 之后,Java EE 模块被从 JDK 中逐步移出,比如javax.xml.bind(JAXB)。如果你项目里还有 XML 解析相关的代码,可能还会遇到ClassNotFoundException: javax.xml.bind.JAXBException。这种情况需要在pom.xml里单独引入jakarta.xml.bind-api,或者干脆检查一下是否还有旧代码在使用这些库。
我在迁移时做了个笨但有效的事:在 IDEA 里全局搜索import javax.这个前缀,把结果列出来,一个一个看。很多代码只是 import 语句需要替换,没有其他逻辑变化。
2.4 渐进式接入:先让空项目跑起来再谈 Shiro
我推荐的做法是:先不要一口气把 Shiro 配置全部搬过去,而是在一个干净的 Spring Boot 3.2 项目里只加 Shiro 依赖,不加任何 ShiroConfig,不加过滤链,先启动一次看基础环境是否正常。
这一步的意义在于:把“依赖迁移”和“配置迁移”这两个问题隔离开来。如果连依赖都有问题,你后边加配置时看到的报错会把原因混在一起,排查成本翻倍。
我当时是先建了一个 demo 项目,把 Spring Boot 3.2 跑起来,确认 Tomcat 正常启动、端口正常显示,然后再开始写 Shiro 的配置类。事实证明这个决策非常明智,后面遇到启动异常时,我基本可以断定问题出在 Shiro 配置上,而不是底层环境。
3. 配置类翻新:从 ShiroFilterFactoryBean 到链式定义的完整改写
3.1 旧的 ShiroFilterFactoryBean 配置为什么在新版失去意义
如果你以前用 Spring Boot 2.x + Shiro 1.x,大概率见过这种配置:
@Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); factoryBean.setLoginUrl("/login"); factoryBean.setUnauthorizedUrl("/403"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/**", "authc"); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; }这套配置在 Shiro 1.x 时代完全没问题,但在 Shiro 2.0.1 里,ShiroFilterFactoryBean已经被移除了。我第一次编译时看到“程序包 org.apache.shiro.spring.web.config 不存在”还以为是版本拉错了,后来翻官方文档才确认:Shiro 2.0 的 Web 集成方式改成“链式定义”了。
这背后还有一个更底层的逻辑:Shiro 2.0 不再自己去创建 Filter 实例并强行注入 Servlet 容器,而是把过滤器的构建和注册交给 Spring Boot 的自动配置机制。你只需要告诉 Shiro “我要拦截哪些路径”,剩下的由它自动完成。这个变更大大减少了对javax.servletAPI 的直接依赖,也是它能迁移到jakarta的基础之一。
3.2 新配置样板:SecurityManager 和 ShiroFilterChainDefinition
下面是 Shiro 2.0.1 在 Spring Boot 3.2 下能直接用的配置类骨架:
import jakarta.servlet.Filter; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.servlet.ShiroFilter; import org.apache.shiro.web.spring.config.ShiroFilterChainDefinition; import org.apache.shiro.web.spring.config.DefaultShiroFilterChainDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfig { @Bean(name = "securityManager") public DefaultWebSecurityManager securityManager(Realm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); return securityManager; } @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); Map<String, String> filterChainMap = new LinkedHashMap<>(); filterChainMap.put("/login", "anon"); filterChainMap.put("/logout", "logout"); filterChainMap.put("/**", "authc"); chainDefinition.addPathDefinitions(filterChainMap); return chainDefinition; } }这里有几个关键点:
第一,SecurityManager的 Bean 名字必须是securityManager。Shiro 自动配置会以这个名字去容器里找DefaultWebSecurityManager,如果名字不对,运行时会提示找不到 SecurityManager 相关的 Bean。
第二,ShiroFilterChainDefinition替代了以前的filterChainDefinitionMap。路径匹配规则和以前一致,anon表示匿名访问,authc表示需要认证,logout表示退出。
第三,如果你需要自定义登录地址,可以用:
FormAuthenticationFilter filter = new FormAuthenticationFilter(); filter.setLoginUrl("/login");但要注意,如果项目是纯前后端分离的 REST 风格,FormAuthenticationFilter默认的 302 跳转行为会让前端很痛苦。这个坑我后面详细说。
3.3 自定义 Realm:登录和权限的核心
Realm 是 Shiro 的身份数据源,相当于用自己的账号体系对接 Shiro。一个典型的自定义 Realm 长这样:
import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import java.util.HashSet; import java.util.Set; public class JwtRealm extends AuthorizingRealm { @Override public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 这里模拟从数据库查用户 String username = (String) token.getPrincipal(); if (!"admin".equals(username)) { throw new UnknownAccountException("用户不存在"); } return new SimpleAuthenticationInfo(username, "123456", getName()); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); Set<String> roles = new HashSet<>(); Set<String> permissions = new HashSet<>(); if ("admin".equals(username)) { roles.add("admin"); permissions.add("user:add"); permissions.add("project:create"); } SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(permissions); return authorizationInfo; } }这段代码和 1.x 时代几乎没什么区别,说明 Shiro 2.0 在核心认证授权模型上是向后兼容的,主要变化集中在 Web 层和 Servlet 容器集成层。
3.4 注解权限的补丁:别让 @RequiresPermissions 无故失效
使用@RequiresPermissions这类注解时,需要 Spring AOP 和 Shiro 的 Advisor 配合。在我的项目里,光有shiro-spring-boot-web-starter还不够,还要手动声明两个 Bean,否则注解权限会静默失效:
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.mgt.SecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ShiroAnnotationConfig { @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator(); creator.setProxyTargetClass(true); return creator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }很多人在 Spring Boot 3.2 下升级 Shiro 2.0.1 后,发现接口不鉴权了,以为是自己权限配置写错,其实只是 AOP 代理没生效。这个 Bug 不会报任何错误,只会让你所有权限注解变成摆设。
4. 踩坑实录:两天内遇到的四个致命异常和完整排查链
4.1 ClassNotFoundException: javax.servlet.Filter——开门第一坑
这是最典型、最令人崩溃的错误。我当时运行mvn spring-boot:run,控制台前几行还正常,然后突然抛出一大段异常日志,最上面写着:
java.lang.ClassNotFoundException: javax.servlet.Filter第一反应是依赖里少了javax.servlet-api,于是往pom.xml里加了这个依赖,结果启动后变成了另一个错误:java.lang.NoClassDefFoundError: javax/servlet/Filter。这是因为 Tomcat 10 里根本没有javax.servlet.Filter这个类。
排查链路是这样走的:
- 先确认内嵌 Tomcat 版本。Spring Boot 3.2 默认 Tomcat 10.1.x,它只实现
jakarta.servlet。 - 再看 Shiro 版本。
shiro-core1.x 的所有 Web 模块都是基于javax.servlet编译,所以只要 Shiro 还是 1.x,这个错误就是必然。 - 最后用
mvn dependency:tree -Dincludes=javax.servlet看依赖来源,把不需要的旧依赖排除掉。
最终的修复就是升级到 Shiro 2.0.1,并确保没有别的库把javax.servlet-api拉进来。如果你是降级回来或者非要兼容老版本,那就得用 Spring Boot 2.7 以下的版本,别在 Boot 3.2 上折腾。
4.2 启动日志不显示端口号:不是端口冲突,是 Context 初始化直接失败
有一个现象很误导人:项目启动后,控制台看不到Tomcat started on port 8080这行日志,看起来像端口被占用,或者启动被卡住了。我之前就盲目地执行了lsof -i:8080查看端口,结果什么都没查到。
后来才明白,这种“静默失败”往往是因为 Spring 容器在刷新阶段就已经抛了异常,只是异常堆栈被吞掉了一部分,日志只显示到 Spring Boot 的 banner 和启动信息就断了。
排查方法:
mvn spring-boot:run -Dspring-boot.run.arguments=--debug开启 debug 模式后,真正的Caused by才会显示出来。在我的场景里,根因是 Shiro 的自动配置尝试创建一个ShiroFilter,但这个 Filter 类型是org.apache.shiro.web.servlet.ShiroFilter,它同样需要jakarta.servlet.Filter接口。因为这个接口找不到实现,整个 WebApplicationContext 初始化失败,自然也就轮不到 Tomcat 打印端口号了。
如果你在 IDEA 里启动 Spring Boot 项目,看到控制台只输出了 banner、没有端口号,第一步别去查端口占用,直接往下翻异常堆栈,或者开 debug。大部分情况下,这是容器初始化失败,不是端口问题。
4.3 登录接口 302 到 /login:FormAuthenticationFilter 的传统路径逻辑
当 Shiro 的过滤链配置了/ **为authc后,所有未认证请求都会被拦截。如果在配置里设置了loginUrl,Shiro 会对未认证请求发起重定向。
对于传统服务端渲染的页面项目,这种方式没问题;但现代项目大多是前后端分离,前端用fetch或axios调用接口,遇到 302 时浏览器会自动跟随重定向到/login,返回一个 HTML 页面,前端拿到的不是 JSON,解析直接失败。
当时我们项目的登录接口是/api/login,前端请求/api/project/list时收到的重定向是/login,导致前端控制台报了一堆 404 或者 HTML parser 错误。
解决方式有两种:
一种是自定义过滤器,拦截未认证请求时直接返回 401 JSON 响应,不重定向。这个方案更符合前后端分离架构。
另一种更简单:把/login等认证接口配置为anon,同时把loginUrl指向一个不存在的路径,让未认证请求进入认证逻辑而不是重定向。但这种方法不够优雅,我后来还是花时间写了一个无状态的认证过滤器。
4.4 rememberMe 和 Session 的未授权异常,被安全扫描逼出来的重构
Shiro 1.x 的 rememberMe 机制默认使用 Java 序列化,配合硬编码密钥,这是安全扫描重点关注的对象。升级到 Shiro 2.0.1 后,我特意检查了 rememberMe 的配置,发现如果还沿用旧代码的硬编码方式,安全扫描依然会报高风险。
建议至少做到两点:
第一,把 rememberMe 的密钥放到配置文件中,并通过环境变量注入,不要写死在代码里。
第二,如果项目本身是接口型服务,建议尽量用 JWT 替代 Session 机制。这样可以让 Shiro 的 Subject 状态不依赖服务端 Session,减少跨域和重启失效的问题。当然,这会涉及更大的重构,不是简单改配置就行。
Shiro 2.0.1 本身已经做了很多安全增强,但底层设计上 rememberMe 仍然是一把双刃剑。我的原则是:非必要不开启,必要则必须使用随机加密密钥,并且定期轮换。
5. 上线前对照清单:怎么验证这次迁移没有埋雷
5.1 代码检查清单
如果你也是从 Shiro 1.x 升级到 2.0.1,上线前先过一遍这个清单:
- 全局搜索
import javax.servlet.,确认所有 Web 相关 import 都已切换为jakarta.servlet.。 - 检查
pom.xml中是否还有javax.servlet-api依赖,能用exclusion排除就排除,不要留着。 - 检查
web.xml、ServletRegistrationBean、FilterRegistrationBean中是否有直接引用javax.servlet类型的地方。 - 确认
SecurityManager的 Bean 名称为securityManager,避免自动配置找不到。 - 确认
ShiroFilterChainDefinition中路径匹配顺序正确,anon路径一定要放在/**之前,否则会被authc拦截。
5.2 功能回归用例模板
我在迁移完成后会执行一套固定的回归用例,你可以直接参考:
| 测试项 | 预期结果 | 说明 |
|---|---|---|
| 未登录访问受保护接口 | 返回 401 或重定向到登录页 | 取决于你选择的认证方案 |
登录成功后访问/api/project/list | 返回 200 和业务数据 | 验证认证过滤器正常放行 |
使用@RequiresPermissions("user:add")的接口,未授权用户访问 | 返回 403 | 验证注解权限和 AOP Advisor 生效 |
| 退出登录后再次访问受保护接口 | 返回 401 | 验证 Session 清理正常 |
| 正常登录后等待 Session 过期 | 再次请求被拦截 | 验证会话管理正常工作 |
| rememberMe 开启后重启服务 | 部分场景下登录态可持续 | 不要在纯 API 场景默认开启 |
这里特别提醒,路径匹配规则的顺序非常重要。Shiro 的过滤器链遵循“先匹配先生效”的原则,如果/login的anon写在/ **的authc后面,那anon实际上永远不会生效。
5.3 回滚与灰度策略
迁移这种底层的认证授权框架,哪怕测试再充分,也建议预留回滚方案。我的做法是:
第一,在 Git 里保留迁移前的分支,并打一个 tag,比如release/shiro-1.13-boot-2.7。这样如果线上出现无法容忍的问题,可以快速切回旧分支重新发版。
第二,不要直接在生产环境做“一次性切换”。先在预发环境跑一个完整的回归周期,重点观察 Session、权限、登录接口的响应时间变化。Shiro 2.0 的 Session 管理和 1.x 在底层实现上有差异,某些极端情况下旧 Session 无法复用,用户会被强制下线,需要提前通知相关方。
第三,如果必须灰度,可以先让少量用户走新版本网关,通过流量比例控制逐步放量。不过在实际操作中,认证授权框架通常不适合细粒度灰度,因为它影响所有接口。所以我的建议是“预发充分回归 + 生产快速回滚预案”的策略,比“少量灰度”更实在。
最后说点个人体会
这次从 Spring Boot 2.7 + Shiro 1.13 迁移到 Spring Boot 3.2 + Shiro 2.0.1,整个过程最折磨人的不是配置类的改写,而是那些藏在第三方依赖里的javax引用。它们平时安安静静躺在依赖树里,一旦你切换到 Jakarta 容器环境,就会在运行时的某个角落突然跳出来捣乱。
我的一个经验是:遇到这种底层基础框架升级,先别急着改业务代码,一定要先把依赖树和容器版本确认清楚。用mvn dependency:tree花半小时扫一遍,比上线后花了几个小时排查异常堆栈要划算得多。
另外,建议完整浏览一遍 Shiro 2.0 的官方文档,重点看 Spring Boot 集成部分的“兼容性说明”。按照我的实际体验,Shiro 2.0 的核心 API 变化不大,但 Web 层集成方式确实换了思路,只要把 SecurityManager、ShiroFilterChainDefinition 和 AOP 支持这三件事理顺,整个迁移就成功了大半。
如果你接下来也要做类似升级,希望这篇踩坑实录能帮你少走一些弯路。我最后再推荐一个小习惯:在所有 import 路径上都用jakarta.*,并且尽量不要在项目里保留任何javax.*的 Servlet API 依赖,干净清爽才能睡得安稳。