ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

多模块Maven项目云服务器部署实战:从构建到systemd全流程指南

2026/10/8 2:36:07 拓冰建站 浏览量
多模块Maven项目云服务器部署实战:从构建到systemd全流程指南 最近帮团队把一个多模块Maven项目从开发机发布到云服务器前后折腾了一整天。整个过程不算复杂但涉及的点特别碎模块依赖怎么理顺、本地构建该打哪个包、传上服务器之后环境对不对、进程怎么拉起、出了问题去哪看日志。中间踩了不少坑整理成这篇记录希望能帮你少走弯路。这篇东西适合正在接手多模块Maven项目、准备做首次云上部署的Java开发同学也适合那种“本地能跑、换台机器就熄火”的情况。我会从多模块结构设计讲起到本地构建、云服务器环境准备、最后是部署排查全程按实操路径走。1. 先拆清楚多模块项目为什么绕不开“聚合”和“继承”很多刚接触多模块项目的人有一个误解以为把几个Module放在同一个工程里就是多模块了。真正难的是模块之间的依赖关系、版本管理、还有构建顺序。Maven解决这个问题靠的是两个核心机制聚合Aggregation和继承Inheritance。1.1 Maven父子模型一个父工程管住所有版本号聚合的意思是把多个模块放到一个父工程下父工程只干一件事声明modules列表然后你在这个父工程目录里执行mvn installMaven会自动按依赖顺序依次构建子模块。继承则是把所有公共配置抽到父POM里比如依赖版本、插件版本、公共属性子模块可以不写groupId和version直接继承父工程配置。我强烈建议把版本号统一收口到父POM的dependencyManagement里。举个最简单的父子结构!-- 父工程 pom.xml -- groupIdcom.example/groupId artifactIddemo-parent/artifactId version1.0.0/version packagingpom/packaging modules moduledemo-common/module moduledemo-service/module moduledemo-web/module /modules properties java.version17/java.version spring.boot.version3.2.5/spring.boot.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring.boot.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement如果你去看过那种治理得比较好的项目父POM里很少放具体dependencies都是放dependencyManagement因为dependencies会被子模块无条件继承而dependencyManagement只是提供版本约束子模块需要哪个依赖自己声明坐标就行。这个区别非常重要我见过不少项目把所有依赖一股脑写在父POM里结果每个子模块都背着一堆无关的jar启动变慢不说还容易冲突。1.2 微服务模块如何拆分从common-service-web三层看边界模块怎么拆直接影响打包和部署的体验。常见的拆法有按层拆common、service、web和按业务拆order、user、product。我的经验是中小型项目优先按层拆模块一旦超过七八个构建速度会明显下降这时候再考虑按业务边界拆。举个按层拆的例子demo-parent ├── demo-common # 工具类、DTO、基础配置 ├── demo-service # 业务逻辑、数据访问 ├── demo-web # Web入口、Controller └── demo-application # 启动类、装配层这里有一个很容易踩的坑把Spring Boot的启动类放在父工程或最底层模块里。实际上启动类应该放在最上层的web或application模块因为只要它一启动就会触发整个依赖链的加载。如果把启动类放在common模块改一次代码commons Group的整个版本都要重新发布非常被动。各模块相互依赖时需要在一个父子关系清晰的前提下保持单向依赖web依赖serviceservice依赖common。如果出现common反过来依赖web这种事先别急着打包先回去把模块边界重新划一遍。2. 本地构建三板斧clean、install、还要会挑模块多模块项目打一次完整构建少则几十秒多则几分钟。如果每次改一行代码都要全量install太浪费时间了。这一步我会把构建顺序、参数、组合指令都讲透。2.1 理解了生命周期你就理解了Maven在干吗Maven构建有完整生命周期validate、compile、test、package、verify、install、deploy。执行mvn install时会按顺序执行到install为止所以install既包含package又比package多做一步——把产物安装到本地仓库供其他模块引用。这也是为什么多模块项目里一定要用install而不是package如果只packagedemo-web依赖demo-service时Maven从本地仓库找不到刚构建的demo-service版本于是去中央仓库或者私服找结果必然是找不到。实际项目里我常用的几个指令组合# 构建并跳过测试发布到本地仓库 mvn clean install -DskipTests # 跳过代码检查、测试等一切非必要环节 mvn clean install -DskipTests -Dcheckstyle.skiptrue -Dmaven.javadoc.skiptrue # 也给测试用例留条后路 mvn clean install -DtestUserServiceTest -DfailIfNoTestsfalse这里说一下-DskipTests和-Dmaven.test.skiptrue的区别。-DskipTests只是不执行测试代码但测试类还是会被编译-Dmaven.test.skiptrue连测试代码扫描和编译都能跳过速度更快。开发环境用前者纯构建上线用后者可以把构建时间再压缩十秒八秒。2.2 多环境构建一套代码三套配置从本地到测试服到正式服数据库地址、Redis地址、日志级别这些配置一定不一样。如果每次手动改配置文件再打包很容易把测试配置误带到线上。我的做法是结合Maven Profile和Spring Boot的多环境配置文件实现一套配置、按需构建。先在父POM里声明profileprofiles profile iddev/id properties profile.activedev/profile.active /properties activation activeByDefaulttrue/activeByDefault /activation /profile profile idprod/id properties profile.activeprod/profile.active /properties /profile /profiles然后让资源文件读取这个属性把application-${profile.active}.yml这类配置文件里的占位符替换成实际值或者干脆在启动脚本里通过spring.profiles.activeprod指定。我实际更推荐后者构建时不必做资源过滤部署时用环境变量或者-Dspring.profiles.activeprod控制环境灵活性高很多一个jar包在所有环境都能用这就是“构建一次到处运行”。2.3 挑着构建只想打某个模块时用-pl和-am全量构建费时间很多场景下你其实只需要构建有改动的模块。Maven有两个参数用来做模块级构建# -pl指定构建哪些模块-am构建模块所依赖的模块 mvn clean install -pl demo-web -am -DskipTests第一次跑这条命令时你会看到Maven连demo-common、demo-service一起构建因为demo-web依赖它们这就是-amalso make的作用。如果依赖模块已经发布到本地仓库且没有改动也可以去掉-am只构建指定模块速度能快一大截。平时开发完单测之后我基本只用这条组合mvn clean install -pl demo-web -am -DskipTests既保证依赖分支最新代码又不会白白编译不相干的模块。3. 云服务器环境准备JDK版本与运行时规划构建产物拿到手下一步就是上服务器。这一步最容易出问题的反而不是“怎么传文件”而是“服务器运行环境跟你的本机不一致”。本地能起、服务器起不来90%都是环境问题。3.1 先确认Java版本别让class文件报UnsupportedClassVersionError前端时间我接手一个老项目本机用的JDK 17编译完之后传到服务器上一跑直接报UnsupportedClassVersionError服务器装的是JDK 8。这种问题其实很好查但也很好避免。先在本地确认编译版本和服务器运行版本是否一致# 本地查看编译版本 javap -verbose target/classes/com/example/Application.class | grep major # 服务器查看JVM版本 java -version版本对应关系参考这张表Java Major版本说明52JDK 855JDK 1161JDK 1765JDK 21如果你用的是Spring Boot 2.7那Java 8、11都可以Spring Boot 3.x就要求Java 17起。别因为本地是更高版本的JDK编译出来就默认服务器也能跑这是新手最容易忽视的问题。服务器上装好对应JDK之后记得设置JAVA_HOME环境变量。很多排查不出来的进程异常最后都指向JAVA_HOME没配好导致用错了jdk。我习惯在/etc/profile.d/java.sh里单独建一个文件统一管理避免污染主profile。3.2 部署机上要不要装Maven从构建到产物分离说起典型的云服务器部署有两种做法一种是把源码拉上去在服务器上构建另一种是本机构建好之后只上传产物。我强烈建议用后者服务器上不装Maven、不装Git甚至源码都不放。理由有三点第一服务器更干净攻击面更小。第二构建环境一致不会出现“你这台机器上编译出来怎么跑不起来”这类环境漂移问题。第三服务器磁盘空间本来就不宽裕本地仓库动辄几个G没必要浪费在部署机上。那什么时候才需要在服务器上装Maven一般是用Jenkins之类的CI工具做流水线构建或者你只有一个便宜的小机器实在没有本机构建条件。这种情况下装Maven 3.8就行注意用mvn -version确认Maven和JDK版本匹配。Maven 3.9.x要求JDK 8Maven 4.x就要JDK 17起步了。3.3 安全组、端口与数据库连接串一个都不能少生产环境部署和本地最大的区别在于网络层面。云服务商都有安全组策略即使服务监听在0.0.0.0:8080安全组没放行8080端口外部也访问不到。我遇到过不止一次“进程明明启动了页面却打不开”最后发现是安全组没加端口。检查三步走确认服务监听地址。Spring Boot默认绑0.0.0.0如果改过server.address127.0.0.1外部肯定访问不了。在服务器上先本地验证curl http://localhost:8080/actuator/health。再从本地测试连通性telnet 服务器公网IP 8080。另外数据库连接串也要注意云服务器上连数据库别用localhost要写内网地址或公网地址并且确认数据库端口的访问权限。很多RDS默认只允许白名单IP访问服务器IP没加进去之前你怎么连都是超时。我在部署前一般会把“服务器外网IP 端口 数据库地址 数据库账号”这几项提前列个清单逐项核对省得启动后一个坑一个坑地填。4. 打包产物上线systemd、日志与回滚三部曲环境就绪jar也传上去了接下来就是把jar跑起来。java -jar app.jar是最直白的做法但会上不了生产台面。至少要做到进程能开机自启、日志能按天切割、版本出问题能快速回滚。4.1 从打包产物到可执行服务nohup与应用目录规划先规划好目录结构我常用的标准目录/opt/demo-app ├── bin/ # 启动、停止脚本 ├── conf/ # 外部化配置文件 ├── logs/ # 运行日志、GC日志 ├── lib/ # jar包 └── backup/ # 历史版本备份传jar包我一般用scp或者rsync文件不大没必要搞一套复杂的上传平台。scp demo-web/target/demo-web-1.0.0.jar root服务器IP:/opt/demo-app/lib/如果用nohup试跑先确认能启动cd /opt/demo-app nohup java -Xms512m -Xmx512m -jar lib/demo-web-1.0.0.jar --spring.profiles.activeprod logs/app.log 21 --spring.profiles.activeprod会覆盖jar包内嵌的环境配置后面配置外置也会用到类似思路。先跑裸命令是为了快速验证环境和jar包本身没问题确认无误再上systemd把它管起来。4.2 用systemd管理Java进程重启、开机自启、崩溃拉起nohup方式的最大问题是进程意外退出后没人管服务器重启后服务也不会上来。systemd是Linux下标准进程管理方式写一个service文件解决这些事。# /etc/systemd/system/demo-app.service [Unit] Descriptiondemo-app Afternetwork.target [Service] Typesimple Userappuser WorkingDirectory/opt/demo-app ExecStart/usr/bin/java -Xms512m -Xmx512m -jar /opt/demo-app/lib/demo-web-1.0.0.jar --spring.profiles.activeprod ExecStop/bin/kill -s TERM $MAINPID Restarton-failure RestartSec10 StandardOutputappend:/opt/demo-app/logs/app.log StandardErrorappend:/opt/demo-app/logs/error.log EnvironmentSPRING_PROFILES_ACTIVEprod [Install] WantedBymulti-user.target启用并启动systemctl daemon-reload systemctl enable demo-app systemctl start demo-app systemctl status demo-app journalctl -u demo-app -f # 看启动日志注意我的注解Restarton-failure表示非正常退出才会重启手动stop不会拉起这是合理行为。RestartSec10给数据库等外部依赖留出恢复时间避免疯狂重启把服务打挂。一个容易被忽略的细节我用Userappuser跑服务不直接用root。因为Java进程一旦被攻击root权限出事就不可控了。生产环境这条一定要坚持哪怕麻烦一点。4.3 配置外置与日志分流别把生产环境当成本地打包时环境配置可以通过profile控制但生产环境的数据库密码、密钥这些敏感信息最好别打进jar包。Spring Boot支持外部配置优先于jar内配置所以生产环境的配置文件可以直接放在conf/目录启动时指定# 在service文件中追加 --spring.config.additional-location/opt/demo-app/conf/这样一来配置和代码分离换数据库、改连接串都不需要重新打包重新启动即可生效。我自己管理生产环境时会再严格一点conf/备份在Git里任何修改都走提交记录方便出问题回溯操作。日志方面如果直接把System.out和logback日志都输出到同一个文件排查问题时候会很痛苦。我在systemd里用两条StandardOutput/StandardError配置把标准输出和错误流分开再加上logback的滚动策略按天分割、保留30天!-- logback-spring.xml 关键片段 -- appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_PATH}/app.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern${LOG_PATH}/app.%d{yyyy-MM-dd}.log.gz/fileNamePattern maxHistory30/maxHistory /rollingPolicy /appender进程的GC日志也不要漏掉JVM参数加上-Xlog:gc*:logs/gc.log:time,uptime,level,tagsJDK 11的格式出现内存问题时有一手资料可以排查。4.4 发布与回滚脚本哪怕手动发布也要留后路很多团队即使有流水线最终也要依赖手动脚本兜底。我写了一版最基础的发布脚本每次发布前备份当前jar包启动新版本失败就自动回滚# bin/deploy.sh 核心流程 APP_NAMEdemo-web JAR_NAMEdemo-web-1.0.0.jar LIB_DIR/opt/demo-app/lib BACKUP_DIR/opt/demo-app/backup # 1. 备份当前版本 if [ -f $LIB_DIR/$JAR_NAME ]; then cp $LIB_DIR/$JAR_NAME $BACKUP_DIR/$JAR_NAME.$(date %Y%m%d%H%M%S) fi # 2. 新包放到lib目录 cp ~/upload/$JAR_NAME $LIB_DIR/ # 3. 重启并健康检查 systemctl restart demo-app sleep 15 if ! curl -sf http://localhost:8080/actuator/health /dev/null; then echo health check failed, rollback... systemctl stop demo-app cp $BACKUP_DIR/$(ls -t $BACKUP_DIR | head -1) $LIB_DIR/$JAR_NAME systemctl start demo-app fi健康检查没有通过就自动回滚这比人工看一眼页面再决定靠谱得多。这个脚本不复杂但能承载绝大多数发布需求。再进阶一点的做法是打完新包之后先在本机启动一次跑两条冒烟测试命令确认没问题再上传能省去很多来回传包的时间。5. 常见问题与排查技巧实录这节是压箱底的部分把多模块Maven项目在打包部署过程中最常见的坑和排法整理成速查表。5.1 本地能起服务器起不来八成是JDK或模块依赖问题现象分两类一类是UnsupportedClassVersionError那就是JDK版本对不上。另一类是NoClassDefFoundError或者ClassNotFoundException多半是打包出来的jar不完整或者依赖模块没install到位。多模块项目里尤其容易出这个错demo-web依赖demo-service但demo-service改动之后没有重新install到本地仓库你直接用mvn package -pl demo-web打出来的web包引用的还是旧版service类。解决办法在本地构建部分已经说了用clean install -pl demoweb -am让依赖模块一起重新构建。还有一个隐蔽问题打包后检查jar包目录。很多新手不知道用jar tf demo-web.jar | grep com/example/来确认关键类是否存在。上线前养成这个习惯能过滤掉一半以上的低级错误。5.2 端口通不通先别甩锅给代码遇到“页面打不开”我排过最多次的步骤是先ss -tlnp | grep 端口看监听状态再curl localhost:端口看本地响应最后才想到安全组。ss能看到监听没有问题curl不通多半是防火墙拦截curl通而外部访问不通那才轮到检查云安全组。以下按顺序检查# 1. 确认监听 ss -tlnp | grep :8080 # 2. 确认本地响应 curl -I http://127.0.0.1:8080 # 3. 确认防火墙放行 firewall-cmd --list-all # 4. 确认云安全组配置 # 在云服务商控制台查看入方向规则是否放行8080说句实在话我遇到“外部不通”的案例里一半以上是安全组没放行剩下的主要是监听地址写成了127.0.0.1。5.3 数据库时区错乱的经典排查部署到云服务器后时间显示比正常时间早8小时或者晚8小时。这个问题的根源通常是连接串缺了serverTimezone参数或者服务器时区没设置。MySQL连接串加上参数jdbc:mysql://内网地址:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai同时检查服务器时区timedatectl set-timezone Asia/Shanghai date -R连接串指定了时区而服务器时区也对但日志里时间还是不对再看应用日志框架的pattern配置文件里有没有写死UTC如果logback配置里用了%d{yyyy-MM-dd HH:mm:ss}默认会取JVM默认时区如果配了UTC那就统一加{Asia/Shanghai}比如%d{yyyy-MM-dd HH:mm:ss,Asia/Shanghai}。为了这种事排查一小时的案例确实不少。5.4 Maven仓库依赖下载失败换源与离线包两条路构建时最让人崩溃的体验是从中央仓库拉依赖拉十分钟超时了断点续传又不灵活clean之后从头再来。国内开发必须有镜像仓库这一课。在~/.m2/settings.xml或Maven安装目录的conf/settings.xml里加镜像mirrors mirror idaliyun/id nameAliyun Maven Mirror/name urlhttps://maven.aliyun.com/repository/public/url mirrorOfcentral/mirrorOf /mirror /mirrors如果某个私服或中央仓库的包死活拉不下来还有一个办法在有网的环境把依赖包打成离线仓库上传到目标机器然后修改settings.xml指向本地目录。比如mvn dependency:go-offline -f pom.xml这种方式适合云服务器上网络受限的场景但日常开发完全没必要离线配好镜像基本畅通。5.5 构建慢、反复下载依赖理解settings.xml和本地仓库settings.xml里还有一个高频配置本地仓库地址。默认在~/.m2/repository如果你换过电脑或重装过系统旧仓库全部丢失下次构建要从头拉。公司里如果有统一的私服其实可以把本地仓库路径指向一个共享大盘多人复用。个人项目则无所谓认准这个路径即可。热词里有个问题很常见“我有两个Maven本地仓库怎么合并”说实话没什么完美的合并方案本地仓库本质是目录缓存最平稳的做法是保留一个主仓库另一个仓库里未被包含的目录直接拷贝过去然后让Maven重新索引。遇到镜像下不下来的旧版本包时这个拷贝方式特别救急cp -rn old-repository/* ~/.m2/repository/注意别用cp -r直接覆盖-n保留现有文件不会因为拷贝操作把新仓库已有的合法包覆盖坏。在最终说一句个人体会多模块项目的打包部署从来不是“写代码”的范畴而是“交付”的范畴。代码写得再漂亮最后一个环节发布翻车前面的努力全被埋没。所以把构建、打包、部署这套流程打磨成可重复、可回滚的标准化操作比临时抱佛脚调一次好运值更有价值。另外一个小建议部署完成之后把构建用的Maven命令、服务器上的目录结构、systemd配置这些统统写进项目README或者单独一份DEPLOY.md。半年后你自己回来看也会感谢这份记录。