信创改造实战:SpringBoot项目迁移到宝兰德BES 9.5.5的完整避坑指南
1. 项目概述与迁移前思考
先说背景。之前接手了一个典型的SpringBoot 2.7.13微服务项目,原来部署方式是mvn package打成可执行jar包,扔到服务器上nohup java -jar就完事。但客户侧明确要求走信创路线,中间件必须国产化替换,目标平台锁定为宝兰德BES Application Server 9.5.5。这意味着原先的内嵌Tomcat方案不能用了,必须把SpringBoot应用以war包形式部署到BES上,由BES接管HTTP服务和类加载环境。
这里有个核心认知必须先纠正:SpringBoot项目迁移到BES,不是简单地把jar改成war扔进去就能跑。BES本质上是完整Java EE应用服务器,它有自己的一套类加载体系、JNDI命名空间、安全管理器和部署规范。内嵌Tomcat方式下很多“约定大于配置”的默认行为,在外部Servlet容器环境下会失效。比如SpringBoot自动装配里的ServletWebServerFactoryAutoConfiguration,在没有内嵌容器依赖时根本不会生效;再比如spring.main.web-application-type的推断逻辑,也会因为类路径变化而改变。
这篇博文适合谁看?一是正在做信创迁移、需要把存量SpringBoot服务部署到BES或其他国产中间件(如东方通TongWeb、金蝶天燕等)的开发人员;二是刚接触BES、想了解这个中间件部署机制的技术同学。我会把依赖改造、打包配置、控制台部署、以及我实际踩过的问题完整列出来,基本可以按图索骥。
先交代一下本次迁移涉及的核心组件清单,后面所有配置都是基于这个版本组合验证过的:
- 操作系统:CentOS 7.6(x86_64)
- JDK:1.8.0_202(BES 9.5.5内置OpenJDK或使用外部JDK均可,但推荐统一用外部JDK)
- 应用框架:SpringBoot 2.7.13 + MyBatis-Plus 3.5.x
- 目标中间件:宝兰德BES Application Server 9.5.5
- 构建工具:Maven 3.6.3
- 原部署方式:内嵌Tomcat 9.0.x jar包方式
2. BES 9.5.5环境准备与安装要点
2.1 获取安装包与基础环境
BES 9.5.5的安装包可以从宝兰德官方渠道获取,注意区分版本——企业版安装包通常包含管理控制台、节点代理和完整的Java EE容器;开发版可能阉割了部分集群能力,但单机部署完全够用。我这边拿到的是Linux 64位安装包,文件名类似bes-linux-x64-9.5.5.tar.gz。解压后的目录结构大概如下:
# 解压安装包 tar -zxvf bes-linux-x64-9.5.5.tar.gz -C /opt/bes/ cd /opt/bes/ ls -lh # 核心目录说明 # bin/ 启动脚本、管理命令 # domains/ 域目录(创建域之后生成) # lib/ 中间件核心依赖 # modules/ 模块化组件 # property/ 配置文件模板注意:BES安装路径不要包含中文和特殊字符,建议统一放在
/opt/或/usr/local/下,否则后续启动脚本会报路径解析异常。
2.2 创建域与启动管理控制台
BES的部署模型和WebLogic很像,核心概念就是“域(Domain)”。一个域包含管理服务器(AdminServer)和若干被管节点(Managed Server),所有应用部署操作都通过管理控制台完成。单机部署时,可以只创建一个域,管理服务器和业务服务器共用同一个实例,减少资源开销。
创建域的两种方式:
方式一:命令行方式
cd /opt/bes/bin ./asadmin create-domain --adminport 4848 --port 8080 mydomain这里adminport是管理控制台端口,port是HTTP服务端口。asadmin是BES自带的域管理命令,类似GlassFish的asadmin,用起来很顺手。
方式二:交互式方式
直接执行./asadmin create-domain,不加参数,它会引导你输入域名、端口、管理员账号密码。我建议用交互式,方便设置管理员密码。
创建成功后,启动域:
./asadmin start-domain mydomain启动日志会输出到domains/mydomain/logs/server.log,看到类似Server startup in [xxxx] milliseconds就说明启动成功了。此时访问http://服务器IP:4848可以进入管理控制台。
2.3 JDK版本选择与配坑
BES 9.5.5兼容JDK 8和JDK 11,但我的项目是SpringBoot 2.7.13(基础要求JDK 8),所以统一用JDK 8运行BES。这里有个比较隐蔽的坑:BES默认会使用自己内置的JRE(如果你没配置外部JDK),而这个内置JRE的版本不一定和应用的编译版本匹配,极容易导致UnsupportedClassVersionError。
建议强制指定外部JDK路径。修改domains/mydomain/config/domain.xml或启动脚本中的JAVA_HOME变量:
# 编辑启动脚本或设置环境变量 export JAVA_HOME=/usr/local/jdk1.8.0_202 export PATH=$JAVA_HOME/bin:$PATH # 重启域 ./asadmin restart-domain mydomain另外,BES的JVM参数默认堆内存比较保守(通常只有256M),对微服务来说肯定不够。在控制台的“服务器配置 -> JVM设置”里,把-Xms1024m -Xmx2048m -XX:MaxMetaspaceSize=512m配置进去,确保应用运行期内存充足。
提示:BES对JDK的检测逻辑是从启动脚本所在环境变量中读取,不是读
/etc/profile。所以每次启动前务必保证JAVA_HOME已经export到当前shell,否则它会fallback到内置JRE,这个问题非常隐蔽,排查起来也很耗时间。
3. SpringBoot项目依赖改造(核心章节)
3.1 打包方式从jar切换为war
迁移第一步是修改pom.xml的打包方式和依赖。原来的spring-boot-starter-web自带了内嵌Tomcat,在外部容器部署时会产生冲突,必须排除掉。
下面是我经过验证的完整依赖配置:
<packaging>war</packaging> <properties> <java.version>1.8</java.version> <spring-boot.version>2.7.13</spring-boot.version> </properties> <dependencies> <!-- Web依赖,排除内嵌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> <!-- BES API,用于自定义ServletInitializer时引用 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- 其他业务依赖:MyBatis-Plus、Redis、Nacos等按需保留 --> </dependencies>关键点有两个:
spring-boot-starter-tomcat必须显式排除。如果不排除而直接打包成war,BES部署时会检测到内嵌Tomcat相关的类,触发类加载冲突,表现通常是启动报NoClassDefFoundError: org/apache/catalina/...,或者应用启动到一半直接卡死。javax.servlet-api的scope必须是provided。因为这个依赖在BES运行时由容器提供,打不打包进去都行,但如果不加这个依赖,代码里引用ServletContext等API时编译会报错。为什么用javax而不是jakarta?因为我们用的SpringBoot 2.x底层是javax规范,BES 9.5.5同时支持javax和jakarta,但2.x用javax没问题。
3.2 SpringBootApplication启动类改造
SpringBoot jar方式部署的启动类通常长这样:
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }这种写法在war方式部署时有一个致命问题:main方法只负责本地开发启动,在外部Servlet容器中启动时,容器根本不会执行main方法。容器启动的是ServletContainerInitializer体系,所以需要另外提供一个SpringBootServletInitializer的子类,重写configure方法:
@SpringBootApplication public class DemoApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { // 参数必须传当前启动类 return builder.sources(DemoApplication.class); } }注意:sources(DemoApplication.class)不能传错。有人习惯传this.getClass(),如果被代理类包装过,可能造成Spring扫描不到配置类,应用会启动但是无法加载任何Controller。
3.3 application.properties关键调整
接下来说说配置文件。jar方式下,很多外部化配置可以直接通过--spring.profiles.active=xxx方式和nohup命令传参;但war方式部署到BES后,应用是由BES的部署模块加载的,System.getProperty不一定能拿到JVM启动参数,所以有几种做法:
第一种(推荐):在application.properties中直接固定Profile和上下文路径。
# 指定当前环境为生产 spring.profiles.active=prod # 上下文路径,即访问根路径 server.servlet.context-path=/demo # 外部容器模式下,端口配置不生效,由容器负责监听 server.port=8080这里强调一下:server.port在war包部署到外部容器后是无效的!因为端口由BES实例决定,不是应用自己决定。配置了也不报错,只是SpringBoot会忽略它。
第二种(进阶):如果多个应用部署到同一个BES,不同应用想要不同Profile,可以用spring.application.json环境变量或者JNDI自定义配置。这块后续在“多应用隔离”部分再展开,单应用部署先不折腾。
3.4 数据源与JNDI绑定(另一个大坑)
在jar方式下,数据源通常用spring.datasource.url、spring.datasource.username直接写在配置里,或者在Nacos配置中心统一管理。但企业级应用部署到BES后,通常走JNDI数据源方式——这是Java EE容器的标准玩法。原因很简单:运维可以通过控制台动态调整数据库连接池,不用重新打包重新部署;数据库密码可以存放于BES的配置领域,避免明文写在应用配置里。
BES控制台配置JNDI数据源:
- 进入控制台 -> 资源 -> JDBC -> 连接池 -> 新建。
- 数据源类型选择MySQL(我用的是MySQL 8.x,驱动类名
com.mysql.cj.jdbc.Driver)。 - 配置JDBC URL、用户名、密码。
- 设置JNDI名称,比如
jdbc/mydb。 - 点击“测试连接”,确认连通性。
应用侧改造可以二选一:
方案A:Spring Boot标准方式
# 使用JNDI数据源 spring.datasource.jndi-name=jdbc/mydb这是最简洁的写法,SpringBoot的DataSourceAutoConfiguration会自动通过JndiDataSourceLookup去容器中查找数据源。
方案B:自定义DataSource配置类
@Configuration public class DataSourceConfig { @Bean @Primary public DataSource dataSource() throws NamingException { JndiDataSourceLookup lookup = new JndiDataSourceLookup(); return lookup.getDataSource("jdbc/mydb"); } }如果你用了MyBatis-Plus,记得确保DataSource优先被注入到SqlSessionFactory中。方案A就走SpringBoot默认的自动装配路径,基本不用改代码;方案B适合那些需要自定义连接池参数(如最大连接数、超时时间)的场景。
我在实操中发现一个细节:BES的JNDI查找名称,必须和控制台上定义的名称完全一致,开头是java:comp/env/jdbc/mydb还是直接jdbc/mydb,取决于BES版本和配置方式。9.5.5版本在控制台创建连接池时,填入的JNDI名称直接写jdbc/mydb,应用侧写jdbc/mydb就能找到;但如果通过domain.xml手写资源,可能需要加java:comp/env/前缀。最稳妥的办法先用控制台创建,不要直接改XML。
4. 完整构建与BES部署实操
4.1 Maven构建war包
依赖都改好后,执行打包:
mvn clean package -Dmaven.test.skip=true -Pprod构建成功后,war包位于target/目录,名称形如demo-1.0.0.war。注意SpringBoot的SpringBootServletInitializer机制下,war包在本地./target目录中会包含WEB-INF/lib-provided目录(存放provided依赖的jar),这些jar在部署到BES时不会使用,但保留在包内没问题,BES会自动忽略。
4.2 控制台部署war包
部署过程很简单:
- 登录BES管理控制台(
http://IP:4848)。 - 左侧菜单选择“应用” -> “Web应用” -> “部署”。
- 上传war包,应用名称填
demo(建议和war包名一致,这样上下文路径好控制)。 - 上下文根路径填
/demo,对应访问地址是http://IP:8080/demo。 - 部署完成后点击“启动”。
如果一切正常,BES会解压war包,创建应用实例,通过SpringBootServletInitializer完成Spring容器的初始化。查看日志确认效果:
tail -f /opt/bes/domains/mydomain/logs/server.log看到以下日志说明启动成功:
Root WebApplicationContext: initialization completed in 8122 ms BES Web Application Startup Time: 8000 ms然后你就可以访问http://IP:8080/demo/验证接口了。
4.3 单机多实例部署模式
如果服务器资源充足,想在一台机器上跑多个BES实例做应用隔离或本地灰度,可以创建多个域,或者在同一域下创建多个独立实例。命令方式:
./asadmin create-node --nodeport 28181 mynode ./asadmin create-instance --node mynode --port 8081 instance1 ./asadmin create-instance --node mynode --port 8082 instance2创建后分别启动:
./asadmin start-instance instance1 ./asadmin start-instance instance2实际项目中,多实例的典型场景是“nacos注册中心同一个服务注册两个实例,实现本地双节点负载”。BES自身也有集群和负载均衡能力,但如果你的服务中间接入了Nacos/Feign,BES的集群LB反而可以不用,让Nacos层面的负载来分摊流量。
4.4 访问日志与并发参数调优
BES部署后,默认并发和线程池参数比较保守。如果你的SpringBoot应用压测发现吞吐上不去,先看BES的HTTP线程池配置:
控制台 -> 服务器配置 -> HTTP服务 -> 线程池,把最大线程数从默认值调高(比如调到400),然后配置连接超时和请求超时。这些参数对应domain.xml里类似这样:
<thread-pool max-thread-pool-size="400" min-thread-pool-size="20" idle-thread-timeout-seconds="1200"/>访问日志默认是关闭的,排障时需要开启。在JVM选项里加系统属性:
-Dcom.bes.webserver.log.access.enabled=true -Dcom.bes.webserver.log.access.dir=/opt/bes/logs开启后,每次HTTP请求会记录IP、时间、URL、状态码和耗时。排查某接口慢或客户端异常断开时,这份日志非常管用。
5. 实战中的常见问题与排查实录
5.1 类加载冲突:Jackson版本被容器覆盖
问题现象:应用启动报错,提示jackson-databind的某个类方法找不到,或者反序列化时直接抛出InvalidDefinitionException。定位后发现BES自身lib目录带了一份较旧的Jackson版本,由于BES的类加载策略是“父类优先”,应用lib中的新版本Jackson被旧版本覆盖。
解决方案:
- 修改应用的
WEB-INF/classes下的META-INF/MANIFEST.MF,通过Class-Path显式指定优先加载应用jar。这个在BES里对应“应用部署配置 -> 类加载策略”,可以设置为“子类优先”(即child-first)。 - 或者在
domain.xml中给特定应用添加如下配置:
<application name="demo" class-loader-policy="child-first"/>如果不想动配置,还有一个土办法:把目标jar(如jackson-databind新版本)复制到/opt/bes/domains/mydomain/lib/目录,覆盖BES自带的旧版本。但这种方法会影响域内所有应用,改动面较大,如果不是确认全局兼容,不建议直接覆盖。
5.2 启动出现“No active profile set”或配置未加载
问题现象:应用日志打出No active profile set, falling back to 1 default profile,数据库连接失败,判断是官方配置没生效。
原因分析:war部署在BES时,SpringBoot的application.properties路径不受--spring.config.location控制,同时nacos地址等参数需要通过环境变量或JNDI传入。排查时先确认spring.profiles.active是否正确注入,再看nacos.config.server-addr是否可通过本地环境变量读到。
推荐做法:
# application.properties保持默认配置 spring.profiles.active=${SPRING_PROFILES_ACTIVE:prod} spring.cloud.nacos.config.server-addr=${NACOS_ADDR:127.0.0.1:8848}这样BES的JVM系统属性或操作系统环境变量里配置SPRING_PROFILES_ACTIVE和NACOS_ADDR就能自动覆盖。如果环境变量也拿不到,就把NACOS_ADDR硬编码后重新打包,等后续有条件再接配置中心。
5.3 上传文件临时目录不存在
问题现象:通过接口上传文件(比如Excel导入),第一次报错Failed to create temp directory或者java.io.IOException: No space left on device,但磁盘空间明明够用。
原因分析:SpringBoot默认上传下载的临时目录取自javax.servlet.context.tempdir,BES为每个应用分配的临时目录在domains/mydomain/applications/demo/tmp,如果该目录不存在或权限不对,就会报这个错。
解决办法:
mkdir -p /opt/bes/domains/mydomain/applications/demo/tmp chown -R bes:bes /opt/bes/domains/mydomain/applications/然后在应用配置里强制指定临时目录:
spring.servlet.multipart.temp-dir=/opt/bes/domains/mydomain/applications/demo/tmp5.4 日志文件找不到了
jar方式下,logback-spring.xml里的日志路径通常写成相对路径./logs,在war方式下相对路径的“当前目录”是BES的bin目录或特定工作目录,结果日志文件被创建到奇怪的地方,甚至因为权限不足,直接抛异常无法输出日志。
经验做法:把logback-spring.xml中的日志路径改成绝对路径,或者通过LOG_HOME系统属性指定:
<property name="LOG_HOME" value="${LOG_HOME:-/opt/bes/domains/mydomain/logs/demo}"/> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/app.log</file> <!-- 其余配置省略 --> </appender>然后启动BES前设置系统属性或环境变量:
export LOG_HOME=/opt/bes/domains/mydomain/logs/demo5.5 集群部署时Session共享问题
如果有多个BES实例组成集群,默认情况下Session不会跨节点共享。SpringBoot应用如果用到HttpSession(登录验证码、权限暂存等),需要把Session共享方案迁移到容器级别。
BES自带集群Session复制功能,但配置比较繁琐;我们项目里选择在应用层解决:接入Redis统一存储Session。依赖如下:
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>配置:
spring.session.store-type=redis spring.redis.host=127.0.0.1 spring.redis.port=6379这样不需要动BES集群配置,Session问题就直接绕开了。
6. 依赖配置与避坑清单速查
下面把本次改造的核心内容浓缩为一张速查表,方便你迁移时对照检查:
| 检查项 | jar包内嵌Tomcat方式 | war包部署BES方式 | 备注 |
|---|---|---|---|
| 打包方式 | <packaging>jar</packaging> | <packaging>war</packaging> | 必须改 |
| web-starter | spring-boot-starter-web | spring-boot-starter-web(排除tomcat) | 排除内嵌Tomcat |
| servlet-api依赖 | 无需 | 添加javax.servlet-api,scope=provided | 不添加则编译报错 |
| 启动类 | 普通main方法 | 继承SpringBootServletInitializer | 必须重写configure |
server.port | 生效 | 不生效,端口由BES管理 | 配置了也不会启动监听 |
server.servlet.context-path | 生效 | 建议在BES控制台配置上下文根 | 配置可能被覆盖 |
| 数据源 | spring.datasource.url直接配置 | JNDI数据源spring.datasource.jndi-name | 也可以从Nacos读取 |
| 日志路径 | 相对路径./logs | 必须使用绝对路径 | 否则日志散落各处 |
| JVM参数 | java -jar -Xmx | BES JVM设置中配置 | 与控制台保持一致 |
| 上传临时目录 | Servlet容器自动管理 | 需显式指定 | 否则可能创建失败 |
| Profile切换 | 命令行--spring.profiles.active | 环境变量或JVM系统属性 | 需保证可注入 |
这份表其实不限于BES,也适用于其他国产Java EE中间件,比如东方通TongWeb、金蝶天燕Apusic、中创InforSuite等,它们的部署方式和常用坑都高度相似,换成对应控制台操作就行。
7. 我踩过的最深的坑
最后分享一个让我排查了两天的典型问题,给你打个预防针。
我们是先从jar方式在本地mvn spring-boot:run调试,功能全通了,再改成war方式部署到BES。部署后应用界面能开,但所有接口返回的JSON中中文字段都变成了???或乱码。一开始怀疑是数据库连接字符集问题,检查characterEncoding=utf8也加了,没用;又怀疑是HttpMessageConverter配置问题,排查了一大圈。
最后用curl直接观察BES的响应头,发现Content-Type里带的是charset=ISO-8859-1,而不是预期的UTF-8。原来是BES的HTTP响应编码默认走ISO-8859-1,SpringBoot在外部容器下拿不到请求默认编码,所以返回体的编码被覆盖了。
解决办法:在BES控制台的“服务器配置 -> HTTP监听器 -> 编码”里把默认编码改成UTF-8,或者在应用的Filter里强制设置response.setCharacterEncoding("UTF-8")。后来为了避免每个接口都手动处理,我加了一个OncePerRequestFilter统一处理:
@Component public class CharacterEncodingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); filterChain.doFilter(request, response); } }部署后再测,中文全部正常。
这个坑的教训是:中间件迁移过程中,“看似正常”比“直接报错”更可怕。报错时日志会给出线索,而编码这种隐性问题,很容易被误判为业务代码问题。建议你在做信创改造验证时,专门抽出测试用例覆盖中文字符、特殊符号、上传下载文件流、Session传递这几个高敏感场景,每一个都要盯紧。
迁移完成后,记得把BES日志接入你的ELK或日志平台,后续线上排查完全依赖它。祝你的信创改造之路一切顺利,少踩一些我踩过的坑。
提示:以上配置基于宝兰德BES 9.5.5版本,如果你使用的是其他版本,部分控制台路径和参数名可能会有差异,以官方文档为准。如果迁移过程中遇到其他问题,欢迎一起交流学习。