ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot迁移到宝兰德BES 9.5.5实战:war包部署与踩坑指南

SpringBoot迁移到宝兰德BES 9.5.5实战:war包部署与踩坑指南 前阵子接了个活儿把一个跑了好几年的SpringBoot单体应用迁移到宝兰德BES 9.5.5上。说白了就是信创改造里最典型的一步——把应用从开源Tomcat搬到国产中间件。整个过程不算复杂但坑是真不少。网上关于SpringBoot部署到BES的资料不多很多细节都是进了现场才发现的所以我把这次迁移的完整过程和踩坑记录整理出来给后面做同类改造的同学省点时间。这次改造涉及的内容包括为什么选BES 9.5.5而不是继续用Tomcat、SpringBoot版本如何和BES的Java EE规范对齐、war包怎么打、内嵌容器怎么排除、部署到管理控制台之后出现了哪些诡异的类加载和日志问题以及最后怎么调通数据源和静态资源。如果你手头也有一个SpringBoot项目要迁移到国产中间件这篇记录可以直接照着走。1. 迁移前的方案选型为什么从Tomcat迁到宝兰德BES1.1 信创改造中的中间件替换逻辑最开始接到需求时团队里有人提出一个很实在的问题SpringBoot默认就内嵌了Tomcatjava -jar跑得好好的为什么还要单独装一个BES这个问题几乎每个刚开始做信创改造的人都会问。答案在于信创改造并不是只看应用本身能不能跑还要看整条技术链路的国产化覆盖情况。客户在验收时通常会核查操作系统、数据库、中间件这几个关键组件是否在国产化目录内。SpringBoot内嵌的Tomcat属于开源组件虽然应用层还是自己的代码但中间件这一层没有完成替换整个交付物就无法通过合规审查。而且很多单位的运维体系要求中间件统一管理要有管理控制台、集群能力、监控接口和审计日志。这些能力如果自己用Tomcat拼工程量很大直接用BES这类国产应用服务器基本开箱即得。所以我给出的结论是迁移到BES不是技术上的倒退而是把应用运行载体从内嵌容器切换成独立中间件。SpringBoot本身只是一个开发框架它完全可以跑在外部Servlet容器上只是大多数开发者习惯了内嵌Tomcat忘了SpringBoot还有war包部署这条传统路线。1.2 BES 9.5.5和SpringBoot的兼容性怎么看宝兰德BES 9.5.5是一款兼容Java EE 8规范的应用服务器。这个版本信息很关键它直接决定了SpringBoot项目的选型方向。Java EE 8时代Servlet规范对应的是javax.servlet命名空间到了Jakarta EE 9之后命名空间才改为jakarta.servlet。BES 9.5.5既然兼容Java EE 8就意味着它对javax命名空间的支持最成熟。所以SpringBoot项目如果还在用javax体系迁移成本最低基本不需要改代码里的Servlet相关引用。基于这个前提我建议优先考虑以下版本组合项目组合兼容性评估说明SpringBoot 2.7.x JDK8 javax最稳妥与BES 9.5.5的Java EE 8模型完全对应社区案例最多SpringBoot 2.7.x JDK11一般可行需确认BES所在主机有对应架构的JDK11SpringBoot 3.x JDK17 jakarta风险较高BES 9.5.5默认面向javax需联系厂商确认补丁或后续版本支持情况我自己实际用的组合是SpringBoot 2.7.18 JDK8。之所以没有上SpringBoot 3除了命名空间兼容性之外还有一个现实原因信创环境经常搭配的麒麟V10、统信UOS等操作系统上JDK8的对应架构发行版最好找国产化适配也最成熟。团队里如果还有老工程师不熟悉新版本语法JDK8的学习成本也最低。1.3 迁移路径从内嵌容器到外部war想明白为什么迁和迁到什么版本之后迁移路径就清晰了。SpringBoot默认启动方式是java -jar app.jar应用内嵌Tomcat自己监听端口自己跑。部署到BES之后的启动方式变成BES加载war包应用本身不再负责监听HTTP端口而是由BES统一管理连接、线程池、会话和生命周期。这个变化会带来一串连锁反应打包方式从jar变成war内嵌Tomcat的依赖要么排除、要么改成provided启动类需要继承SpringBootServletInitializerserver.port配置可能不再生效端口由BES决定日志、配置文件的默认路径可能指向BES安装目录静态资源、JSP、Filter、Listener的加载顺序和类加载机制全部由外部容器接管。很多团队在迁移时只改了一个打包方式结果启动报各种ClassCastException和NoSuchMethodError就是因为没意识到这是一次运行模型的变化。后面几个章节我会把每个环节拆开详细说。2. 迁移前准备和版本对齐2.1 SpringBoot版本、JDK版本怎么定版本对齐是迁移之前最不该偷懒的环节。我见过有人直接把SpringBoot 3.2项目打成war丢到BES里结果启动类加载阶段直接报jakarta.servlet相关的ClassNotFound整个应用起不来最后不得不返工改命名空间。如果你也面临类似情况我的建议是不要纠结直接用SpringBoot 2.7.x的最终版本也就是2.7.18。这个版本是SpringBoot 2.x生命周期里最成熟的一个大量信创项目都验证过碰到问题能找到的参考资料也最多。JDK版本方面强烈推荐JDK8。不是说JDK11不行而是信创环境下JDK8的发行版最多、兼容性最好。比如飞腾、鲲鹏、龙芯这些平台上基本都有对应的JDK8发行版。安装JDK时要注意架构匹配在ARM架构的机器上装x86的JDKjava -version能装出来但跑起来迟早出问题。装完之后用java -version确认一下同时看一下系统架构是不是一致。另外提醒一件事BES本身是Java应用也要运行在同一个JDK环境上。BES要求使用哪个JDK版本以宝兰德官方安装手册为准。你应用用的JDK和BES用的JDK最好保持一致避免应用编译目标版本和容器运行版本不一致出现各种莫名其妙的UnsupportedClassVersionError。2.2 改成war包后的构建细节版本确定之后第一步是修改Maven的打包方式。packagingwar/packaging就这一行构建产物从app.jar变成app.war。简单但影响深远。原来的target目录结构是BOOT-INF/classes、BOOT-INF/lib改完之后变成标准的Servlet规范结构WEB-INF/classes、WEB-INF/lib。这个变化意味着外部容器可以按照标准方式加载你的应用。这里有一个附带问题spring-boot-maven-plugin的repackage目标。如果POM里保留了repackage配置打成war之后它会生成一个可执行war也就是这个war既能丢到BES里部署也能用java -jar app.war直接跑。可执行war内部会包含org/springframework/boot/loader相关类依赖里必须保留提供spring-boot-loader的依赖同时内嵌Tomcat也要以provided形式存在。如果你只打算部署到BES其实不需要这个能力。但我不建议把repackage完全关掉。因为开发阶段经常还是想本地java -jar验证一下保留可执行war反而方便。只要注意一点war里如果出现了WEB-INF/lib/tomcat-embed-core-*.jar说明provided范围没写对这个包会被BES的类加载器优先加载后续大概率产生类冲突。2.3 剥离内嵌Tomcat的正确姿势既然要部署到BESSpringBoot自带的Tomcat就不能打进war里不然BES启动web应用时容器自带Servlet实现和应用内嵌的Tomcat实现同时存在类加载器分分钟给你表演一场我是谁我在哪的ClassCastException。通常有两种做法第一种在spring-boot-starter-web里排除spring-boot-starter-tomcat然后再单独引入一个provided范围的spring-boot-starter-tomcatdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependency第二种只排除spring-boot-starter-tomcat不再额外添加dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency两种方式都能把Tomcat从war包中剥离出去。区别在于保留一个provided依赖本地用main方法启动SpringBoot时内嵌Tomcat仍然可用开发体验不受影响。只排除不新增的话本地java -jar会直接报错因为类路径里找不到Tomcat的类了。我这次选择了第一种原因就是不想牺牲本地开发调试的便利性。provided范围不会把Tomcat打进war所以对BES部署没有影响是性价比最高的做法。3. 依赖与配置改造的详细过程3.1 POM标准配置参考下面是我这次迁移最终使用的POM核心片段你可以直接参考。除了打包方式和Tomcat处理之外还包含了日志排除和JDK版本声明。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties java.version1.8/java.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties packagingwar/packaging dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependency !-- 其他业务依赖 -- /dependencies这里把spring-boot-starter-logging一起排除掉是有意的。BES自带日志框架如果你用的SpringBoot默认logback两者会在启动时争抢SLF4J绑定轻则刷警告重则日志丢失。后面3.4节我会详细展开。3.2 启动类改造继承SpringBootServletInitializerSpringBoot能通过java -jar直接启动是因为main方法调用了SpringApplication.run。但部署到外部Servlet容器时容器不知道你的main方法在哪它只认标准接口。所以启动类必须继承SpringBootServletInitializer让外部容器能找到SpringBoot应用上下文。改造后的启动类长这样SpringBootApplication public class DemoApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(DemoApplication.class); } public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }注意一点configure方法里传入的DemoApplication.class必须和你当前的启动类是同一个不要抄模板时抄成其他类否则BES启动后访问到的Spring容器是空的Controller全部404。另外如果你的项目里用ServletComponentScan扫描了WebServlet、WebFilter、WebListener这类注解在BES上可能会失效。传统应用服务器对Servlet的注册有自己的一套扫描逻辑和SpringBoot内嵌容器不完全一致。遇到这种情况建议改为用ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean手动注册虽然代码多一点但行为最可预期。3.3 application配置路径、上下文与资源SpringBoot内嵌Tomcat时端口和上下文路径都由application.properties控制。迁到BES之后情况变了。先说端口。部署到BES后HTTP请求先到BES再由BES转发给应用。应用自己配置的server.port基本不会生效。如果你不删掉这个配置应用日志里可能显示Tomcat started on port 8085但实际访问的还是BES监听的那个端口。这种端口错觉在运维排查时特别坑建议把server.port直接注释掉让BES统一管理端口。上下文路径也一样。如果应用之前配置了server.servlet.context-path/demo在BES上部署时这个路径仍然可能生效但实际访问路径还和BES中的应用部署名有关。我建议把上下文路径保持和BES部署名称一致少一层映射就少一个问题。我这次直接设置成server.servlet.context-path/demo然后BES控制台部署war时也把应用名称设为demo。这样访问路径就是http://IP:端口/demo直观不绕。静态资源路径也值得检查。SpringBoot默认从classpath:/static读取静态资源这个在BES下仍然可用。但如果自定义过spring.web.resources.static-locations注意路径必须写classpath:前缀否则外部容器找不到。spring.mvc.static-path-pattern/static/** spring.web.resources.static-locationsclasspath:/static/如果应用有上传文件下载文件的需求还要注意临时目录变化。BES运行时的user.dir指向的是BES安装目录而不是你原来java -jar所在的目录。代码里凡是用了相对路径读写的都要改成绝对路径或者通过配置项动态获取。3.4 日志冲突处理日志冲突是这次迁移里我最想吐槽的一个点。表面上看项目没改任何代码启动时突然出现SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/.../logback-classic-1.2.12.jar] SLF4J: Found binding in [jar:file:/.../slf4j-log4j12-1.7.25.jar]原因是BES自带的日志框架和SpringBoot的logback同时出现在类路径里SLF4J不知道该听谁的。严重时还会因为log4j和logback的桥接冲突抛出NoSuchMethodError应用直接启动失败。解决方案有三种排除SpringBoot默认的logback统一交给BES管理保留应用的logback关闭BES的自带日志用logback-spring.xml做更细粒度的配置强制指定绑定。我这次选的是第一种也就是在POM里排除spring-boot-starter-logging让应用内的日志输出统一走BES的日志体系。这样运维只需要看BES一个地方日志归档、文件清理、监控对接都方便。但要注意排除默认logging之后应用里的org.slf4j.LoggerFactory仍然可用因为BES带了SLF4J API。只有那些直接依赖logback-classic特有API的代码会受影响比如显式调用ch.qos.logback.classic.Logger的场合真遇到这种情况再单独引入logback即可。3.5 数据源与数据库适配信创项目普遍搭配达梦数据库我这次的目标数据库也是达梦。迁移前后数据源的差异主要在三方面驱动类、URL、连接池兼容性。达梦的典型配置spring.datasource.urljdbc:dm://127.0.0.1:5236/DMSERVER spring.datasource.driver-class-namedm.jdbc.driver.DmDriver spring.datasource.usernameSYSDBA spring.datasource.passwordyour_password驱动依赖需要根据达梦官方提供的jar来引入。不同版本坐标不一样有的用com.dameng:DmJdbcDriver18有的直接用本地jar安装到Maven仓库。如果公司有统一私服建议让DBA或者中间件管理员把驱动jar推到私服避免每台机器手动装。连接池方面HikariCP和Druid在BES下都能工作。我这次用的是Druid因为信创项目普遍要求监控SQL和连接状态。但要注意Druid的版本和JDK8的兼容性老版本在JDK8下会报UnsupportedOperationException建议用较新的稳定版。如果你的项目原来用的是MySQL现在要切到达梦那就不能只改依赖和URL了。分页语法、LIMIT关键字、日期函数、自增主键获取方式都不一样。建议迁移前先让开发把所有SQL过一遍做一个方言不兼容清单逐条改写。这个工作量不小但躲是躲不掉的。4. 部署到BES 9.5.5的完整过程4.1 安装BES和访问管理控制台BES的安装过程不算复杂安装包里一般有图形安装向导和静默安装两种方式。我的建议是生产环境用静默安装方便重复执行和自动化交付。安装完成之后启动BES服务访问管理控制台。默认情况下管理控制台的地址一般是http://IP:8080/console或者类似的路径具体端口和上下文以安装完成后的提示为准。首次登录需要设置管理员密码这个密码一定要设成强密码因为BES管理控制台一旦被入侵整个集群的应用都在别人手里了。安装目录建议单独规划。不要装在系统盘也不要把应用war和日志直接丢在BES安装目录里。我习惯建一套目录结构/opt/bes/ ├── bes952/ # BES安装目录 ├── apps/ # 应用war包归档 ├── config/ # 应用外部化配置 └── logs/ # 应用日志这样BES升级或重装时应用和数据不用跟着动。4.2 两种部署war包方式BES部署war包有两种常见方式。第一种是管理控制台部署。登录控制台之后找到部署或应用管理入口上传war包指定应用名称点击部署。这种方式适合单机或者集群规模小的情况界面能看到部署状态遇到启动失败还能直接看日志。第二种是直接拷贝到BES的自动部署目录。很多版本支持把war丢到指定目录后自动解压部署。这种方式适合脚本化发布但需要注意文件权限和部署目录的配置。我这次用的是控制台部署。原因是迁移初期需要频繁看应用启动日志控制台里查看比较方便。上传war包之前我会先手动停掉旧应用再上传新包。如果直接覆盖上传BES可能因为war被占用或解压目录冲突产生奇怪的中间状态。4.3 JVM参数与启动参数传递BES启动web应用时会创建一个或多个JVM实例。JVM参数默认在启动脚本里配置比如setEnv.sh或者start.sh里面的JAVA_OPTS。我这次给应用分配了2G堆内存JAVA_OPTS-Xms2g -Xmx2g -XX:MaxMetaspaceSize512m -Dspring.config.locationfile:/opt/bes/config/application.properties其中-Dspring.config.location是SpringBoot的外部化配置参数用来指向应用自己的配置文件。这样做的好处是war包里不打包生产环境的配置不同环境直接用不同配置文件省得每次发版都要改配置重新打war。还有一个容易被忽略的点应用代码里如果用System.getProperty(user.dir)获取当前目录在BES下拿到的是BES的启动目录不是war解压目录。代码里写相对路径的地方一定要改成从配置中心或-D参数传入的绝对路径。4.4 安全加固与信创合规迁移完成不能光看业务通不通安全和合规层面的检查也很重要。我总结了几条必须做的事第一修改BES管理控制台的默认账号和密码关闭不必要的远程管理端口生产环境建议把控制台绑定到内网管理网段。第二如果应用暴露了SpringBoot Actuator端点必须严格限制访问。特别是/actuator/heapdump如果被外部访问到JVM堆内存里的敏感信息会直接泄露这是SpringBoot常见的安全漏洞之一。最稳妥的做法是只开启health和info并把所有Actuator端点放到内网访问。第三BES对HTTPS、国密算法有支持能力。如果客户要求国密SSL需要参考BES文档配置对应的加密套件。这个在信创验收里经常会被问到。5. 常见问题清单与现场排查方法5.1 高频问题速查表迁移过程中我记录了一批高频问题整理成速查表供参考。现象可能原因处理方式启动报ClassNotFoundException: org.springframework.web.context.WebApplicationContextwar未包含spring-web或BES类加载顺序异常检查依赖确保spring-web存在查看BES是否开启应用优先类加载启动报NoSuchMethodError: javax.servlet.http.HttpServletRequest.getServletContext()应用打包了低版本servlet-api将servlet-api依赖改为provided或删除启动时多个Spring容器同时初始化内嵌Tomcat未彻底排除检查WEB-INF/lib下是否存在tomcat-embed相关jar日志警告SLF4J绑定冲突logback与BES自带日志框架冲突排除spring-boot-starter-logging应用启动成功但页面404上下文路径配置不一致或静态资源路径错误统一server.servlet.context-path和BES部署名称上传文件功能异常user.dir变化或临时目录不存在使用绝对路径存储设置spring.servlet.multipart.location数据库连接反复断开驱动版本不兼容或空闲连接回收策略问题更换达梦驱动版本调整连接池参数访问/actuator/heapdump能下载文件Actuator端点暴露过多限制端点列表或关闭Actuator5.2 案例分析类加载冲突迁移中最容易遇到的一类问题就是类加载冲突。BES这类传统应用服务器有一套复杂的类加载机制父加载器加载容器自身的类子加载器加载应用的类。当应用war里带了和容器重复的类时不同加载器加载出来的类即使包路径完全一样JVM也认为它们是不同的类型强制转换时就抛出ClassCastException。我遇到过的一个典型报错java.lang.ClassCastException: org.apache.catalina.core.ApplicationHttpRequest cannot be cast to org.apache.catalina.core.ApplicationHttpRequest第一眼看到这个报错非常懵前后两个类名一模一样居然还有Cast异常。后来检查war包发现WEB-INF/lib里躺着tomcat-embed-core-9.x.jar。BES用的是自己内部的Servlet容器实现war包里的Tomcat类被应用类加载器优先加载导致同一个类出现两份。解决办法很直接清干净target重新构建确保tomcat-embed-core不进入WEB-INF/lib。排查时用下面这条命令快速检查jar tf app.war | grep tomcat-embed只要在WEB-INF/lib下看到tomcat-embed开头的jar基本就是这个坑。5.3 案例分析NoSuchMethodError另一个高频问题由Servlet API版本不一致引起。应用代码在编译期依赖了较高版本的javax.servlet-api但war包里又打包了一份旧版本运行到某个方法时BES提供的Servlet实现里根本没有这个方法直接抛NoSuchMethodError。我遇到的具体报错是java.lang.NoSuchMethodError: javax.servlet.http.HttpServletRequest.getServletContext()Ljavax/servlet/ServletContext;排查方法很简单查看war包的WEB-INF/lib里有没有javax.servlet-api或者servlet-api的jar。如果有把它从依赖中移除或者改成provided范围让运行期统一使用BES提供的Servlet API。dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId scopeprovided/scope /dependency这个建议也适用于其他应用服务器本来就提供的API比如JSP API、EL API、JTA API。凡是BES能提供的统统用provided避免和容器打架。5.4 验证与性能回归部署成功只代表能启动不代表业务能用。我做迁移后的第一轮验证重点覆盖这几类功能登录认证和会话保持列表查询和分页文件上传、下载Excel导出定时任务WebSocket实时通知。以上功能最容易受容器切换影响因为都涉及Servlet规范、HttpSession、文件处理等底层能力。登录和大文件上传是最容易翻车的点建议优先测。性能方面迁移到BES之后如果感觉响应变慢不要急着甩锅给中间件先看这几点JVM堆内存是否充足、数据库连接池是否够用、线程池配置是否合理、是否因为日志冲突导致大量同步写盘。这些点逐一排查完大部分性能问题都能定位。最后再说一点个人体会。信创改造最大的成本往往不是中间件本身而是应用围绕容器形成的各种隐式约定。SpringBoot的内嵌容器把复杂度藏了起来一旦切到BES这类外部应用服务器很多看不见的依赖都会浮出水面。所以做这类迁移时提前把依赖梳理、日志统一、配置外部化这三件事做好后面会省非常多时间。另外不管多熟悉SpringBoot部署前都要做一次war包内容检查看看WEB-INF/lib里到底有哪些和Servlet容器相关的jar这一步能挡掉80%的启动期问题。
返回列表