先说一个我自己的真实经历:一个跑得好好的SpringBoot 2.7服务,原本依赖内嵌Tomcat直接java -jar启动,结果信创改造要求替换成国产中间件宝兰德BES 9.5.5。我当时以为“不就是换个地方部署嘛”,结果从jar包改成war包、从依赖处理到数据源配置,前前后后踩了一整周的坑。这篇文章就把涉及的依赖配置、打包方式、启动类改造和常见的运行时问题完整写出来,给后面接手同样任务的同学少走点弯路。
1. 先把一个关键认知掰正:BES不是给SpringBoot“跑jar”的,它接管的是war包
很多第一次接触宝兰德BES的Java开发,第一反应是“我项目是SpringBoot,直接拿BES装一个运行环境,然后把jar丢进去不就行了”。这个想法非常危险。BES 9.5.5作为一个完整的Java EE应用服务器,最标准的应用部署单元是war包,它通过自身的Web容器来加载、解析和管理应用。SpringBoot默认的内嵌Tomcat,或者更准确地说,SpringBoot那种java -jar直接启动的模式,在BES上并不是不能跑,但会绕开BES的部署治理体系,也容易在服务管理、日志采集、会话保持这些环节断链。
我这次迁移的目标很明确:让SpringBoot应用真正“跑在BES内部”,而不是在BES旁边再起一个进程。要实现这一点,最初要改的就是部署形态。
1.1 什么是“中间件替换”,它到底替换了什么
日常开发里说“用Tomcat部署”时,大多数时候指的是“Servlet容器”,它只负责处理Servlet、JSP、静态资源这些Web层的东西。宝兰德BES这类国产应用服务器不仅要处理Web层,还提供了完整的Java EE规范支持,比如EJB、JMS、数据源管理、集群能力。信创改造中的“中间件替换”,本质上是把项目原来依赖的Tomcat、Jetty或者WebLogic这类第三方组件,换成BES这个国产中间件,而应用代码本身尽量保持不动。
但SpringBoot和传统Java EE项目的最大区别在于,它默认使用了“内嵌容器 + 可执行jar”的方式分发应用。这意味着Tomcat不是部署在外面的,而是被打包在jar里、由SpringBoot启动时动态创建。要让BES接管,就得把容器的职责“外移”,也就是回到传统的应用部署模型:外部容器负责Web服务,应用以war包形式发布。
1.2 哪些SpringBoot项目不建议硬迁到BES
不是所有SpringBoot项目都适合一次到位迁移到BES 9.5.5。我这次踩完坑之后的最大体会是,迁移之前先给项目做个“容器依赖体检”。如果代码里有以下这些情况,需要先改造再迁:
- 直接引用内嵌Tomcat的类,比如
org.apache.catalina.*、org.apache.tomcat.*; - 自定义了
TomcatServletWebServerFactory、TomcatConnectorCustomizer这类容器定制Bean; - 项目是SpringBoot 3.x,并且用到了
jakarta.servlet命名空间的较新API; - 应用里配置了
server.port、server.tomcat.xxx等容器级配置,并且对端口有强依赖; - 用到了WebSocket、JMX等对底层容器实现依赖较强的组件,没有做标准API封装。
如果你的项目在上述范围里,不要急,先做代码层适配,然后再迁。否则就算war包能部署上去,运行期也会频繁冒出各种和容器实现有关的异常。
2. 折腾前先看版本:BES 9.5.5、SpringBoot版本和JDK怎么选才不吵架
版本选型这个事,一开始是最容易被忽略、后期却最折腾的。我原本图省事直接拿SpringBoot 3.2的项目去迁,结果发现BES 9.5.5的容器协议栈对jakarta.*命名空间的支持并不像我们期望的那样“无缝”,一些Servlet相关API注册完全对不上。后来老老实实把项目降到SpringBoot 2.7.13,才把主要问题解决掉。
2.1 Servlet/Jakarta规范兼容性对照
SpringBoot 2.x系列底层用的是javax.servletAPI,SpringBoot 3.x则切换到了jakarta.servletAPI。BES 9.5.5这套应用服务器,按我的实测和查阅到的资料,更贴合的是Java EE 8时代的规范体系,也就是javax.servlet.*这一套。所以用SpringBoot 2.7.x + JDK8/JDK11组合,是最稳的路线。
我整理了一个兼容性判断表,可以根据项目现状对号入座:
| SpringBoot版本 | Servlet命名空间 | BES 9.5.5部署实测情况 | 建议 |
|---|---|---|---|
| 2.5.x | javax.servlet | 能部署,但部分SpringBoot高版本配置类不兼容 | 老系统可保留 |
| 2.7.x | javax.servlet | 最稳定,和BES的Servlet 4.0/JSP规范匹配良好 | 首选 |
| 3.0.x | jakarta.servlet | 容易出现Servlet注册、日志、自动配置等多个兼容性问题 | 不建议直接迁,先降级 |
如果你手上的业务系统因为某些原因必须用SpringBoot 3.x,那我的建议是先单独做一轮“基础设施适配”,确认BES版本是否已经支持Jakarta EE 9+规范。不要把这个判断寄托在“SpringBoot会自动适配”上,中间件和应用框架之间是双向匹配关系,不是单方面兼容。
2.2 JDK版本选择要注意的细节
BES 9.5.5对JDK版本也有自己的要求。我在本地Windows环境测试时用的JDK 8,后来测试环境切到JDK 11,也能正常跑。但如果你用的是JDK 17甚至更高,建议先详细确认BES自带的字节码增强、热部署和集群服务是否支持。不要因为SpringBoot 2.7支持JDK 17,就觉得BES也一定支持。
另外,安装BES时最好使用中间件厂商提供的JDK适配列表,或者用安装包默认配套的JDK。我自己遇到过一种情况:BES启动脚本里指定的Java路径和我应用编译用的JDK版本不同,导致war部署后在运行期出现UnsupportedClassVersionError。这个问题很隐蔽,因为编译阶段完全正常,到正式加载应用时才会爆炸。
3. 配置清单来了:pom依赖、启动类、打包插件的完整改法
这一节是全文的核心,依赖配置可以直接抄。我以一个常规的SpringBoot 2.7.13项目为例,改造目标是:打包成war、剔除内嵌Tomcat、能被BES 9.5.5正常加载。
3.1 把packaging改成war,并排除内嵌Tomcat
第一步是修改pom.xml中的打包方式。原来是jar,这里必须改成war:
<groupId>com.example</groupId> <artifactId>demo-bes</artifactId> <version>1.0.0</version> <packaging>war</packaging>第二步是处理spring-boot-starter-web依赖。这个starter默认会带进来内嵌Tomcat,如果不排除,打出来的war包会把Tomcat的类也带进去,部署到BES后容易出现“Tomcat类和应用服务器类打架”的情况。正确的做法是把它排除掉:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency>3.2 手动补充Servlet API依赖,scope必须是provided
排除掉内嵌Tomcat之后,编译阶段会缺少Servlet相关API,因为原来这部分的类由tomcat-embed-core提供。此时需要显式引入Java EE规范下的Servlet API,同时必须设置为provided,表示这个依赖最终由外部容器提供,不会打进war包:
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>这一步的位置很重要。如果你用的是SpringBoot 3.x,则对应的是jakarta.servlet-api。但前面讲过,BES 9.5.5更适配javax体系,所以还是建议回到SpringBoot 2.7.x。
3.3 启动类改造:继承SpringBootServletInitializer
原来普通的SpringBoot启动类只需要@SpringBootApplication注解加main方法。部署到外部容器后,为了能让BES在启动Web应用时正确触发Spring容器的初始化,启动类必须继承SpringBootServletInitializer,并重写configure方法:
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; @SpringBootApplication public class DemoBesApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(DemoBesApplication.class); } public static void main(String[] args) { SpringApplication.run(DemoBesApplication.class, args); } }如果项目里有多个@SpringBootConfiguration或自定义配置类作为启动入口,记得在sources里指定正确的那个。否则BES能启动,但Spring容器里加载不到你的业务Bean,会表现出一堆“注入失败”的假象。
3.4 spring-boot-maven-plugin到底要不要管
外部容器部署场景下,spring-boot-maven-plugin已经不负责生成可执行jar了。我的习惯是显式写出来,但关闭repackage功能,避免它对war包做二次包装:
<build> <finalName>demo-bes</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin> </plugins> </build>如果你希望这个war包既能部署到BES,又能在本地用java -jar方便调试,那可以让repackage开着,并给可执行war加一个classifier。但就信创生产环境而言,部署到BES的war包越“干净”越好,混乱的嵌套结构有时候会触发很奇怪的TLD扫描问题。
3.5 一个完整的最小化配置贴在这里
最后把完整的pom关键片段汇总如下,你可以根据自己的项目结构调整:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.13</version> </parent> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- 按需补充其他业务依赖 --> </dependencies> <build> <finalName>demo-bes</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin> </plugins> </build>4. 一个个真实踩过的坑:从启动失败到静态资源404的排查现场
依赖配好了、war包打出来了,不代表就万事大吉。这一章我把迁移过程中亲测遇到的高频问题按“现象-原因-解决”梳理出来,很多坑不是看一眼代码就能定位的。
4.1 部署后启动卡死或直接启动失败:先确认classloader模式
现象是war包在BES管理控制台部署后,应用状态一直在“启动中”,点日志也没有像样的报错,最后Supervise超时。
这个问题我排查了很久,最后定位到是BES的classloader加载策略。应用服务器为了让不同应用之间的依赖互不干扰,默认有一套“父类加载器优先”的逻辑。但SpringBoot的内部结构比较特殊,它有一堆spring-boot-loader和自动配置类。如果外部容器的父加载器优先加载了某些通用类,容易和war包WEB-INF/lib下的同名类产生不可预见的冲突。
解决方式是在BES的管理控制台里,针对这个应用把classloader模式调整成“应用优先”或“parent-last”。其他应用服务器也有类似概念,比如Tomcat里的delegate="false"。改完后重启应用类加载才算真正正常。
4.2 SLF4J日志冲突:LoggerFactory is not a Logback LoggerContext
这是外部容器部署SpringBoot项目的一个经典报错。war包里的应用用的是Logback,BES自带的日志框架可能也绑定了SLF4J,于是运行时出现:
SLF4J: Class path contains multiple SLF4J bindings. LoggerFactory is not a Logback LoggerContext but it could be due to a known logback jar conflict我的处理思路分三步。第一步,确认war包WEB-INF/lib下不要重复打入多余的SLF4J绑定实现;第二步,在logback.xml里显式指定输出策略;第三步,利用前面说的“应用优先”classloader模式,让应用自带的日志实现不被BES父加载器覆盖。
如果应用服务器自带的日志框架确实无法通过classloader隔离,可以考虑让项目改为使用中间件提供的日志适配器。这个改动量较大,一般放到二期优化再做,首要目标是先让应用稳定启动。
4.3 端口占用或应用端口不生效:server.port在BES里说了不算
原来内嵌Tomcat启动时,应用自己监听8080端口。现在部署到BES,8080很可能已经是BES自己用的HTTP端口。我遇到过两种情况:
- 情况一:应用里保留了
server.port=8080的配置,结果SpringBoot尝试再起一个Web服务端口,和BES端口冲突,启动报Address already in use。 - 情况二:改动
server.port后,应用由BES接管,实际访问端口却也没按预期变化。原因很简单,外部容器模式下端口、线程池、连接超时等Web服务器配置都由BES统一管理,SpringBoot层面的server.tomcat.*配置大多失效。
正确做法是,把server.port去掉,端口由BES统一规划。如果你需要多个应用部署在同一个BES实例上,一般通过不同的上下文路径(context path)或虚拟主机来区分。
4.4 war包名变成了访问路径:上下文路径的默认规则
另外一个高频问题是:接口能通,但前端页面打开全是404,或者API路径总是多了个前缀。这是因为war包部署到BES后,应用默认上下文路径就是war包文件名。
比如demo-bes.war部署后,访问入口就是:
http://localhost:8080/demo-bes/如果你的前端资源里的路径写死了/static/xxx.js,自然会因为少了/demo-bes前缀导致404。解决方式有这么几种:
- 接受这个上下文路径,前端资源、nginx或网关转发时统一加上前缀;
- 在BES控制台部署应用时手动指定上下文路径为
/,这样就等价于原Tomcat根路径访问; - 如果是独立部署、没有网关,更推荐显式设置上下文路径并统一维护。
因为生产环境一般前面还有nginx或网关,我建议不要强行把它设成/,否则多个应用在同一个BES下会冲突。
4.5 静态资源和JSP的支持问题
SpringBoot默认对JSP的支持并不好,但BES这类应用服务器天然具备JSP解析能力。如果你项目里混合了静态资源和少量JSP页面,要注意JSP文件的位置。使用war包部署时,JSP页面应该放在src/main/webapp/WEB-INF/views下,而不是放在src/main/resources下。否则war包内部结构不会把JSP放在正确的位置,BES解析时找不到对应视图。
同时,如果确实用到JSP,pom里需要补上JSTL相关依赖:
<dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>还有一个小细节:SpringBoot打成war包后,默认静态资源映射的优先级可能变。遇到原来能访问,迁移后CSS/JS全部404的情况,先看看访问路径里有没有带上上下文路径,再检查WebMvcConfigurer里是否自定义了资源映射。
4.6 数据源该放应用内还是JNDI
SpringBoot项目一般直接在application.yml里配数据源:
spring: datasource: url: jdbc:mysql://localhost:3306/demo username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这种配置方式在BES下依然能用,因为数据源本质上就在应用内创建,和应用服务器无关。但信创环境下,我建议尽量将数据源统一收口到BES中间件管理,也就是用JNDI方式。这样数据库连接数、账号密码策略、监控都能统一维护,切换环境时也不需要重新打war包。
实现步骤是先在BES控制台配置数据源,然后在SpringBoot里通过spring.datasource.jndi-name引用:
spring: datasource: jndi-name: java:comp/env/jdbc/demo-bes要注意的是:一旦配置了jndi-name,原来url、username、password等参数就不需要了。如果两者同时存在,不同版本的BES下行为可能不一致,容易导致数据源初始化失败。
4.7 那些依赖内嵌Tomcat的代码必须提前改写
这个坑技术含量不高,但特别隐蔽。有些项目为了满足特定需求,会注入Tomcat的底层对象,比如通过ServletContext获取TomcatSessionManager,或者在网上抄了一段自定义TomcatConnectorCustomizer用于调整keep-alive参数。这些代码在java -jar模式下跑得很好,但到了BES里,底层容器根本不是Tomcat,代码一执行就抛ClassNotFoundException或NoClassDefFoundError。
解决办法是全局搜索代码里和catalina、tomcat、Tomcat相关的包名,逐步替换成标准Servlet API或Spring Cloud Gateway、负载均衡器层面的配置。
5. 上线前用这份自检表再扫一遍
迁移接近尾声后,我习惯用一张自检表做最终确认。这张表也是我后续帮其他项目做中间件替换时的固定动作,强烈建议打印出来逐项打勾:
| 检查项 | 检查点 | 通过标准 |
|---|---|---|
| 打包形态 | pom.xml中packaging=war | 不是jar或pom |
| 内嵌容器 | spring-boot-starter-tomcat是否排除 | 依赖树中没有tomcat-embed-core |
| Servlet API | javax.servlet-api是否为provided | 最终war包WEB-INF/lib下没有servlet-api |
| 启动类 | 继承SpringBootServletInitializer并重写configure | BES日志能看到Spring容器启动成功 |
| classloader | 应用优先/parent-last | 不再出现SLF4J多绑定或ClassCastException |
| contextPath | 前端/接口访问路径统一 | 静态资源、接口前缀、网关转发一致 |
| JSP位置 | src/main/webapp下且依赖完整 | 视图能正常渲染 |
| Web容器Bean | 无TomcatServletWebServerFactory等自定义Bean | 代码扫描无相关引用 |
| 数据源 | JNDI或应用内数据源单一模式 | 数据库连接建立成功 |
| 日志 | 启动日志、业务日志正常输出 | 无SLF4J警告堆栈 |
| 集群会话 | session实现Serializable | 多节点登录状态正常 |
| 启动方式 | 不再使用java -jar | BES管理控制台完成启动/停止 |
5.1 部署前的备份和回滚设计
这一点看起来是老生常谈,但中间件替换过程中最容易忘记。war包部署到BES后,如果临时需要回滚,不是简简单单把老jar再跑起来就行。
我目前的习惯是:把目前生产环境的可执行jar完整留存,同时保留一份SpringBoot自动配置快照,也就是spring-configuration-metadata这类信息,便于比对迁移前后的配置项差异。BES上部署的应用,回滚时直接通过管理控制台上传旧版本war包即可。整个过程要提前写进变更方案里,不然上线日一到,大家手忙脚乱。
5.2 压测时重点盯哪几个指标
中间件替换后,仅仅功能跑通不算完。我在本地跑过一轮简单的JMeter压测,重点看的是以下三个方向:
- 吞吐量和响应时间变化:由于Servlet容器实现不同,SpringBoot应用从Tomcat迁移到BES后,TPS可能会有10%-20%的波动,需要根据业务指标决定是否需要调BES线程池、连接器参数。
- 内存占用:BES作为完整应用服务器,本身常驻内存高于内嵌Tomcat。压测时观察堆内存和元空间是否存在持续上涨。
- 连接池监控:无论应用内数据源还是JNDI数据源,长期压测下连接池回收是否正常,是否能及时处理连接泄漏。
如果压测阶段发现指标和之前差异过大,第一反应不是去改业务代码,而是先看BES的配置参数,比如最大线程数、acceptCount、连接超时,这些在Tomcat里可能存在application.yml,现在都统一挪到BES的管理控制台或系统配置里了。
6. 写在最后的几点经验
迁移到BES 9.5.5这个事,本质上不是“换部署工具”,而是“变更运行容器”。SpringBoot项目最大的特性就是内嵌和自包含,要把它改造成能被外部应用服务器托管,最核心的动作是把容器的权利交出去:端口交给中间件、类加载交给中间件、日志框架尽量适配中间件。
我个人在实际项目里踩得最深的坑,是低估了classloader隔离对SpringBoot自动配置的影响。很多奇怪问题,比如Bean重复创建、配置属性读不到、定时任务突然不触发,最后都指向“某个类被父加载器加载了”。所以第二次做类似迁移时,我第一件事就是先把classloader模式调好,再开始折腾依赖和代码。
后续如果有条件,可以考虑把BES集群部署和当前项目的会话保持一并做起来。这个扩展步骤同样不复杂,但前提是应用层先保证session等对象可序列化,并且不再使用任何Tomcat私有特性。把这些基础打牢,再做集群配置就顺理成章了。