☰
多模块Maven项目打包部署到云服务器的完整实战指南
2026/10/9 8:14:39 网站建设 项目流程

多模块Maven项目打包构建到云服务器,这活儿看起来简单,真正动手时坑不少。尤其是从“本地能跑”到“服务器上能稳定跑”这段路,涉及的不只是敲几个命令,还有一堆环境、依赖、目录结构上的细节。我这篇就基于自己的实操经验,把这个过程完整拆开讲一遍。项目背景不复杂,一个典型的多模块工程,用Maven管理,最终目标是部署到一台云服务器上对外提供服务。下面从环境准备到构建策略,再到服务器部署、常见问题排查,一条线顺下来,每一环都写清楚“为什么这么做”。

1. 为什么多模块项目部署到服务器,最容易卡在打包这一步

很多人第一次把多模块项目往云服务器上搬的时候,习惯性在服务器上直接执行mvn clean package,然后等它失败。其实这个思路从一开始就跑偏了。本地开发机上的Maven环境、JDK版本、本地仓库里缓存的各种依赖,和服务器上完全是两套东西。服务器上缺一个私有依赖的构件,或者JDK版本不兼容,立刻就是一片红。

这里得先明确一个概念:多模块Maven项目,核心特征是有一个父POM(packaging是pom),下面挂了好几个子模块模块,子模块之间又存在依赖关系。比如典型的订单服务、用户服务、网关模块,可能还有公共的common模块被大家依赖。这个结构在本地IDE里跑没问题,因为IDE会按模块依赖图帮你逐个构建,但到了命令行环境——不管是本地终端还是服务器——就必须由Maven自己解析模块间的依赖顺序。

Maven处理这件事的机制是“reactor”(反应堆)。你在父POM目录下执行打包命令时,Maven会先读取所有模块的POM,构建出一个依赖关系图,然后按拓扑顺序把每个模块编译到本地仓库,再交给下一个依赖它的模块使用。听起来挺顺,但实际操作里有几个非常容易踩的雷:

第一个雷是子模块版本号不一致。比如common模块是1.0.0,订单模块里依赖common时写的却是1.0.1,本地仓库里恰好只有1.0.0,构建就断了。这种问题在多人协作的项目里特别常见,建议所有模块的版本号统一用父POM里的<version>管理,子模块不单独写版本。

第二个雷是reactor模式下某些模块被打包成可执行jar,但依赖的模块还没install到本地仓库。package阶段和install阶段的区别就在这里:package只是在target目录里生成jar/war,并没有塞进本地仓库;如果B模块依赖A模块,而你只对A执行了package,接着对B执行package,B还是从本地仓库找A,大概率找到旧的甚至找不到。所以多模块项目里,clean install几乎是标准动作,而不只是package。

第三个雷是打包插件配置不对。Spring Boot项目里,如果用了spring-boot-maven-plugin,这个插件在repackage阶段会把普通jar改造成可执行fat jar。但如果common模块也引了这个插件,或者父POM里配置了插件但没做skip处理,就会产出一个“既不是普通jar也不是可执行jar”的怪胎。常见的处理方式是只在真正需要启动的模块(比如application模块)启用这个插件,其他模块里配置<skip>true</skip>。

我自己的习惯是,在本地开发机上先把整个项目完整clean install一次,确认所有模块都编译通过且生成了正确的产物,再进入服务器部署环节。这样能把“代码问题”和“环境问题”分开排查,后面遇到服务器上的报错,就可以很自信地判断是环境还是配置的问题。

2. 环境准备:Maven安装、JDK版本和本地仓库的“一次到位”

先聊本地环境,再聊服务器环境。两边很多操作是类似的,但细节不一样。

2.1 JDK版本与Maven版本的对应关系,别在这个细节上翻车

Maven本身是用Java写的,所以运行Maven需要JDK。不同版本的Maven对JDK版本的要求不同,比如Maven 3.8.x要求JDK 1.7以上,Maven 3.9.x要求JDK 1.8以上,而较新的Maven 4.x则建议JDK 17以上。项目本身的编译目标也要和JDK匹配,否则会出现“Maven能运行,但项目编译报错”的诡异情况。

查对应关系最直接的办法是看项目里要求的<java.version>,以及Maven官方文档里标注的Supported Java Versions。我在一台服务器上部署一个老项目时,遇到过Maven 3.9.6配JDK 8的坑:Maven能启动,但maven-compiler-plugin默认的source/target是1.8,一旦代码里用了更高版本的语法就报错。后来我统一把服务器上的JDK升到17,Maven也升到3.9.6,问题才彻底消失。

建议:本地和服务器尽量保证JDK大版本一致、Maven版本尽量一致,差太多会导致本地构建成功但服务器上各种奇怪行为。对于新项目,直接JDK 17 + Maven 3.9.x是比较省心的组合。

2.2 配置Maven的settings.xml:阿里云镜像、本地仓库路径、JDK编译级别

Maven不装任何插件也能实现基础构建,但如果你在网速一般的网络环境里直接拉中央仓库的依赖,那酸爽程度谁拉谁知道。国内开发者一般都会配置阿里云镜像仓库,这已经是默认操作了。

settings.xml文件的位置在$MAVEN_HOME/conf/settings.xml(全局配置)和~/.m2/settings.xml(用户配置)。用户配置优先级高于全局配置。我建议直接改用户配置,这样不影响服务器上其他用户。

一个基本的settings.xml长这样:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd"> <localRepository>/opt/maven/repository</localRepository> <mirrors> <mirror> <id>aliyun</id> <mirrorOf>central</mirrorOf> <name>Aliyun Maven Central Mirror</name> <url>https://maven.aliyun.com/repository/central</url> </mirror> </mirrors> <profiles> <profile> <id>jdk-17</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> </profile> </profiles> </settings>

这里有两个要解释的点。

localRepository默认是~/.m2/repository。如果你不想每次登录用户都换一套本地仓库,特别是服务器上用root或部署账号操作时,显式指定一个固定的repository路径是值得的。比如统一放到/opt/maven/repository,后续看磁盘占用、清理缓存都方便。当然,如果你只有一个固定用户跑构建,用默认路径也没问题。我自己在服务器上更喜欢自定义路径,因为一个项目可能对应多个账号发版,避免每切一个账号就重新拉一遍依赖。

mirrorOf配置为central,意思是只对中央仓库的请求走镜像。如果你有私有仓库(比如Nexus),用*会拦截所有仓库请求,这时候就要按需配置。我之前遇到过一个问题:mirrorOf配成*后,某个内部依赖包从镜像仓库里拉不到,导致构建失败。排查了好久才发现是*把所有仓库请求都指向了阿里云,而阿里云上没有那个包。后来改成central,内部依赖走默认仓库,一切正常。

JDK编译级别放在profile的properties里,比在项目POM里写<maven.compiler.source>更灵活。因为你可能在不同目录下执行Maven命令,有的项目是JDK 8,有的是JDK 17,全局properties只影响设置排版,实际编译目标还是以POM里的配置为准。但是反过来,如果你在POM里没写编译级别,则全局profile这个配置就生效了,所以这里把它作为兜底。

2.3 云服务器安装Maven:下载解压而不是用系统包管理器

很多教程让你直接用apt install maven或者yum install maven,我不太推荐。原因很简单:系统仓库里的Maven版本往往偏旧,比如Ubuntu 20.04自带的Maven可能还是3.6.3,而项目里用了3.9.x的特性,就会出问题。自己下载压缩包解压,版本可控,卸载也干净。

下载地址一般是Apache官方站点,但国内直连速度不稳定。建议从清华镜像源或阿里云镜像下载,速度快得多。下载后解压到/opt/maven,配置环境变量:

export MAVEN_HOME=/opt/maven/apache-maven-3.9.6 export PATH=$PATH:$MAVEN_HOME/bin

为了让环境变量永久生效,把它追加到/etc/profile.d/maven.sh文件里,这样所有用户ssh登录都会自动加载。验证安装:

mvn -version

如果输出里能看到Maven home和Java version信息,说明环境OK。这里有个小细节:执行mvn -version时,Maven会提示“检测到JAVA_HOME”,如果你机器上装了多个JDK,一定要确保JAVA_HOME指向正确的那个。可以通过export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64这种方式显式指定。

3. 多模块项目的父POM与子模块聚合配置,这些决定打包成败

多模块项目能不能顺利打包,第一关就是POM配置是否正确。很多项目在IDE里点“运行”没毛病,一上命令行全暴露了。这里从父POM和子模块两个层面讲清楚怎么配置。

3.1 父POM:packaging为pom、modules列表、依赖管理

父POM的第一个关键点:<packaging>pom</packaging>。只有聚合模块的packaging是pom,Maven才知道要去读取<modules>标签里列的子模块,并按照reactor逻辑处理它们之间的关系。

第二个关键点:<modules>列表。这里列出了所有子模块的相对路径(相对于父POM所在目录)。比如:

<modules> <module>common</module> <module>user-service</module> <module>order-service</module> <module>gateway</module> </modules>

注意,模块顺序并不代表构建顺序,Maven会自己分析依赖关系来排序,所以你不需要手动把common排在第一。但如果你有模块间确实没有依赖关系、纯粹并行构建的情况,可以通过<modules>的顺序影响构建顺序(Maven会尽量按这个顺序启动,但不保证严格串行)。

第三个关键点:<dependencyManagement>。这里统一管理所有子模块的依赖版本号,子模块里引入依赖时可以不写版本,直接从父POM继承。好处是一个地方改版本,全局生效,避免子模块各写各的版本,构建时出现依赖冲突。

第四个关键点:公共插件管理。比如maven-compiler-plugin的版本,spring-boot-maven-plugin的全局配置,建议放在父POM的<build><pluginManagement>里。这样既统一了插件版本,又给了子模块留了覆盖余地。

下面是一份精简的父POM示例:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>parent-project</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <modules> <module>common</module> <module>user-service</module> <module>order-service</module> <module>gateway</module> </modules> <properties> <java.version>17</java.version> <spring.boot.version>3.2.5</spring.boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring.boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <pluginManagement> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring.boot.version}</version> </plugin> </plugins> </pluginManagement> </build> </project>

3.2 子模块:继承父POM、只保留核心配置

子模块的POM要尽量精简。它通过<parent>标签指向父POM,继承版本号、依赖管理和插件管理。子模块自己只需要声明<artifactId>(通常不带version,继承父的)、依赖列表(可以去掉版本号)、以及如果有特殊打包插件则单独配置。

拿common模块举例,它的打包类型默认是jar。因为是被其他模块依赖的公共库,它不该生成可执行jar,所以spring-boot-maven-plugin在这里要skip掉。对应的做法是在它的<build>里这样配置:

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <skip>true</skip> </configuration> </plugin> </plugins> </build>

而user-service这种真正启动的模块,则保持spring-boot-maven-plugin默认配置,让它在Maven的repackage阶段生成可执行jar。

这里有个实战细节:如果子模块里引用了另一个子模块的jar,比如order-service依赖了common,它的POM里这样写:

<dependency> <groupId>com.example</groupId> <artifactId>common</artifactId> <version>1.0.0</version> </dependency>

在reactor模式下,Maven会自动识别这个依赖,并把common的构建产物提供给order-service。前提是common已经被正确install到本地仓库,或者在同一个mvn命令里被先行构建。如果你单独跑到order-service目录下执行mvn package,Maven会从本地仓库找common,本地没有就会报错。所以多模块项目都建议在父POM目录下执行构建,而不是在单个子模块目录里执行。

3.3 关于打包产物的命名与输出目录

默认情况下,每个模块的构建产物生成在自己的target目录下,名字是artifactId-version.jar。如果你希望所有模块的产物集中到一个目录,方便上传服务器,可以给每个模块配置<outputDirectory>或者用maven-antrun-plugin做文件拷贝。我更推荐的方式是构建完成后用一条find命令把所有jar收集到一个临时目录,不额外污染POM配置。

find . -path "*/target/*.jar" -type f | xargs -I {} cp {} /tmp/deploy/

这个操作很暴力,但确实简单有效。特别是模块多、目录层级深的时候,比挨个模块找target目录省时省力。

4. 本地构建的核心命令与构建参数,多模块项目不是只敲一条install那么简单

4.1 完整构建命令与skip测试策略

本地构建时,我最常用的命令是:

mvn clean install -DskipTests -pl . -am

拆开解释:

  • clean:清掉所有模块的target目录,避免旧产物干扰。
  • install:把每个模块安装到本地仓库,供其他模块或后续打包使用。多模块项目构建标准动作就是install,不是package。
  • -DskipTests:跳过测试执行,但会编译测试代码。这样既节省时间,又保证测试代码的语法没问题。
  • -pl .:指定当前目录的模块(即父POM)作为构建的起始点。
  • -am:also make,表示同时构建所有依赖的模块。

如果你希望彻底不编译测试代码,可以用-Dmaven.test.skip=true。这个参数连测试代码编译都跳过,构建更快,适合发版前确认主流程可用、不需要跑测试的场景。但代价是你无法发现测试代码里的编译错误,所以日常开发建议用-DskipTests,发版正式构建时用-Dmaven.test.skip=true。

如果只是想快速构造出生产jar,也可以不加install,用mvn clean package -DskipTests。但前面说过,多模块项目里如果B依赖A,而A没有install,那B的构建还是有可能从本地仓库拉旧的A。所以我的建议是:第一次或关键发版,务必用clean install;日常调试,可以退而求其次用clean package。

4.2 构建时的依赖下载与离线构建思考

第一次执行mvn install会下载大量依赖(尤其是Spring Boot项目),时间可能长达几分钟到十几分钟。这个过程中如果网络不稳定,可能出现下载不完整或checksum校验失败的问题。解决办法很简单:重试。Maven支持断点续传式的重试,但实际上是每次都重来。为了降低网络风险,建议把settings.xml配好镜像仓库,就像前面说的那样。

如果你处于完全离线的环境(比如内网部署),可以提前在本地构建机上执行一次mvn dependency:go-offline,把项目所有依赖缓存到本地仓库,然后把整个本地仓库目录拷贝到服务器上。这是个笨办法,但在某些极端环境下确实有效。需要注意,dependency:go-offline有时候不能抓到所有运行时依赖,如果后续运行时报ClassNotFoundException,再手动补装缺失的依赖即可。

4.3 构建日志:怎么看构建是否成功、产物是否正确

构建成功的标志是构建日志最后出现BUILD SUCCESS。这行输出一般会伴随每个模块的构建摘要,比如:

[INFO] Reactor Summary for parent-project 1.0.0: [INFO] [INFO] common ......................................... SUCCESS [ 0.879s] [INFO] user-service .................................. SUCCESS [ 5.021s] [INFO] order-service ................................. SUCCESS [ 4.143s] [INFO] gateway ....................................... SUCCESS [ 2.001s]

看到这个Summary,说明所有模块都编译打包成功了。如果某一行显示FAILURE,则要看对应模块的详细日志,重点搜索[ERROR]关键字后面的信息,比如缺少依赖、Java语法错误、插件配置错误等。

构建完成后,确认产物的方法有两个:

一是看每个模块的target目录下有没有对应的jar/war文件。比如user-service/target/user-service-1.0.0.jar。

二是把可执行jar包解析一下,确认是fat jar而不是普通jar。可以用jar tf命令查看jar里的内容,如果里面能看到BOOT-INF/classes/、BOOT-INF/lib/这样的目录,说明是Spring Boot的可执行jar;如果只是一个普通的jar结构,说明spring-boot-maven-plugin没生效,或者被skip掉了,这个jar是没法直接java -jar启动的。

5. 云服务器部署实操:文件上传、Java环境、启动脚本

5.1 上传产物的几种方式

构建出的jar包怎么传到服务器上,常见的有几种:

  • scp:简单直接,适合小文件和少量机器。命令形如scp user-service/target/user-service-1.0.0.jar root@server_ip:/opt/app/。
  • rsync:支持断点续传、增量传输,适合大文件或频繁更新场景。命令形如rsync -avz --progress user-service/target/user-service-1.0.0.jar root@server_ip:/opt/app/。
  • git+ CI/CD:如果你用了GitLab CI、GitHub Actions这类平台,一般是构建机拉代码、执行Maven构建,再通过SSH或云原生方式把产物发布到服务器。这个流程更自动化,但需要额外配置CI脚本。
  • 云厂商的对象存储:先传到OSS/COS,再在服务器上从对象存储拉取。适合跨地域分发场景。

我自己在项目早期用的是scp,后来觉得麻烦,干脆在服务器上放了一个deploy.sh脚本,里面封装了从Git拉取代码、Maven构建、停止旧服务、启动新服务的完整流程。这样每次发版只需要在服务器上执行./deploy.sh即可,虽然没上CI/CD,但已经比手动传jar再重启省心太多了。

5.2 服务器上需要安装的Java环境

部署Spring Boot项目,服务器上只需要装JDK(建议JDK 17),不需要装Maven。因为部署阶段只需要java -jar运行,不需要编译源码。除非你打算在服务器上直接做构建(我不推荐),才需要装Maven。

安装JDK的方式有两种:

一是用系统包管理器安装,比如Ubuntu:

apt update && apt install -y openjdk-17-jdk

二是从Oracle或Adoptium下载tar.gz包解压安装,自己控制版本和路径。如果服务器上多个项目需要不同JDK版本,建议用这种方式并配置好JAVA_HOME环境变量。

验证Java环境:

java -version

如果输出里显示openjdk version "17.0.x",说明环境正常。

5.3 停止旧服务、启动新服务的脚本设计

如果jar包是Spring Boot的可执行jar,部署流程就三步:停掉旧进程、替换jar包、启动新进程。但要让这三步自动化,还需要考虑PID文件、日志输出、服务是否真正启动成功等问题。

下面是一个我常用的启动脚本模板,放在/opt/app/user-service/目录下:

#!/bin/bash APP_NAME=user-service-1.0.0.jar PID_FILE=app.pid LOG_FILE=app.log start() { if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then echo "服务已在运行,PID: $(cat $PID_FILE)" exit 1 fi nohup java -jar $APP_NAME > $LOG_FILE 2>&1 & echo $! > $PID_FILE echo "服务已启动,PID: $(cat $PID_FILE)" } stop() { if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then kill $(cat "$PID_FILE") sleep 5 # 检查是否真的停了,如果还在,再kill -9 if kill -0 $(cat "$PID_FILE") 2>/dev/null; then kill -9 $(cat "$PID_FILE") fi rm -f $PID_FILE echo "服务已停止" else echo "服务未运行" fi } case "$1" in start) start ;; stop) stop ;; restart) stop start ;; *) echo "用法: $0 {start|stop|restart}" exit 1 ;; esac

这个脚本细节上做了几件事:

  • 用PID_FILE记录进程号,方便后续停止。
  • 用nohup ... &让服务在后台运行,并把日志输出到app.log。
  • 停止时先尝试kill(优雅停止),5秒后确认进程是否还在,再决定是否kill -9强制结束。

java -jar启动还有一个隐含问题:Spring Boot 3.x默认的嵌入式容器端口是8080,如果你服务器上多个服务都绑定了8080端口,就会冲突。解决办法是在启动参数里指定端口,比如java -jar user-service-1.0.0.jar --server.port=8081,或者更规范一点,把端口配置写到application.yml里,给不同服务分配不同端口。

5.4 服务启动后的验证:不只是“进程还在”那么简单

很多新手看完nohup java -jar启动后,用的是ps aux | grep java看进程在不在,就认为部署成功了。这在简单场景下勉强够用,但一旦服务启动过程中因为配置问题崩溃(比如数据库连接失败、端口被占用),进程会立刻消失,这时候你只能通过日志来排查。

所以建议启动后做三件事:

一是看日志。tail -n 100 app.log查看启动日志,确认没有Exception或ERROR。Spring Boot正常启动的标志是日志里有类似“Started Application in xxx seconds”的语句。

二是检查端口。ss -tlnp | grep 8080查看端口是否被监听。

三是发起一次HTTP请求。如果你的服务提供了健康检查接口(比如Spring Boot Actuator的/actuator/health),可以用curl http://127.0.0.1:8080/actuator/health验证返回是否是{"status":"UP"}。

这三步走完,基本可以放心对外提供服务了。

6. 常见构建与部署问题排查:从日志到根因的完整链路

实战里遇到的坑往往五花八门,这里把最常见的几类问题和排查思路写出来。排查的核心原则永远是:先看日志,再猜原因。

6.1 “Failed to execute goal on project ... Could not resolve dependencies”排查链路

这个报错说明某个子模块的依赖在本地仓库和远程仓库里都找不到。常见的根因和排查顺序:

第一步,确认报错里提示的具体坐标,比如com.example:common:jar:1.0.0。如果这个依赖是项目内部的模块,那问题基本出在“该模块没被install到本地仓库”。

第二步,检查是不是漏了-am参数。如果只构建某个子模块,而它的兄弟模块(比如common)没有被先install,就会报这个错。解决办法是回到父POM目录下执行mvn clean install -DskipTests。

第三步,如果内部依赖确实已经install了,但版本对不上,去本地仓库目录里看看对应版本是否存在。比如本地仓库路径是~/.m2/repository/com/example/common/,看里面是1.0.0还是1.0.1,和POM里引的版本对比。

第四步,如果是外部依赖(比如某个第三方jar包)找不到,先确认settings.xml的镜像仓库是否配置正确,再确认本地仓库里是否有这个目录。有可能是网络或者仓库源的问题。可以手动执行mvn dependency:get -Dartifact=groupId:artifactId:version来测试能否从远程仓库拉取。

6.2 “Disconnected or no reachable”或“Transfer failed”下载类报错

这类报错基本是网络问题,尤其在服务器上下载依赖时。处理顺序:

第一步,先确认服务器能不能访问远程仓库,比如curl -I https://maven.aliyun.com/repository/central/看HTTP状态码。

第二步,确认settings.xml里的镜像URL写对了。阿里云镜像的URL有好几个子路径,分别是/repository/central/(中央仓库)、/repository/public/(聚合仓库)、/repository/gradle-plugin/(Gradle插件)等。如果镜像配错路径,会404。

第三步,检查是不是公司内网要求走代理,如果是,在settings.xml里配置<proxies>节点。这个场景在云服务器上不多见,但如果你部署的服务器在内网环境里(比如私有云),就会遇到。

6.3 Spring Boot fat jar启动时报“no main manifest attribute”

这个报错出现的原因是jar不是Spring Boot的可执行jar,而是普通jar。换句话说,spring-boot-maven-plugin没有正确执行repackage。

排查链路:

第一步,确认当前模块的POM里有没有引入spring-boot-maven-plugin,并且没有配置<skip>true</skip>。如果父POM里通过pluginManagement统一配置了,但子模块没plugins里重新声明,插件也可能不生效。Maven的pluginManagement只提供默认配置,真正执行还要在plugins里引出来。

第二步,跳进jar看结构。用jar tf user-service-1.0.0.jar查看内容,如果只看到com/example/...和META-INF/MANIFEST.MF,没有BOOT-INF/,说明没repackage成功。

第三步,单模块验证。如果这个模块单独mvn package能生成fat jar,但放在多模块reacor中构建却不行,大概率是模块间依赖相加后插件的执行顺序被影响了。可以在子模块的<build>里显式配置spring-boot-maven-plugin,确保绑定到repackage执行阶段。

6.4 端口占用导致启动失败

Web server failed to start. Port 8080 was already in use.

服务器端最常见的坑之一。排查手段:

第一步,ss -tlnp | grep 8080或者lsof -i :8080,确认谁占用了端口。

第二步,如果占用者是旧版本的服务进程(因为停止脚本没生效),就手动kill掉;如果是另一个无关服务占了端口,那就给当前服务换端口。

第三步,如果启动脚本里没有优雅退出逻辑,旧进程可能还活着。这也是我上面脚本里加入kill -9兜底的原因。

6.5 JVM内存不足导致进程直接退出

云服务器默认的物理内存可能不大(比如1G或2G),而Spring Boot应用默认会占用几百MB甚至更多。如果启动时JVM试图申请的内存超过可用内存,会直接报OutOfMemoryError或“There is insufficient memory for the Java Runtime Environment to continue”。

排查与解决:

第一步,启动日志里看有没有-Xmx、-Xms相关的参数提示。

第二步,手动指定JVM内存参数启动,比如:

java -Xms256m -Xmx512m -jar user-service-1.0.0.jar

如果项目是内存敏感的,也可以在启动脚本里统一加这些参数。

第三步,如果服务器内存确实太小,建议升级服务器配置,或者精简运行时的依赖,减小内存占用。

7. 让部署变得更省心的一些个人心得

最后聊几个我自己在实际操作里攒出来的经验。

第一个是关于Jenkins或CI/CD的。如果团队规模不大、项目迭代快,在服务器上手工执行脚本发版也能撑很久。但当模块数量上来、需要频繁发版时,手工操作既容易出错又浪费时间。我自己后来把部署流程挪到了GitLab CI上:推tag自动触发构建,构建产物自动传到服务器,服务器上再通过一个shell脚本更新服务。整个过程不需要人工登录服务器。

第二个是关于配置文件的。Spring Boot默认会把application.yml打在jar里,但不同环境(开发、测试、生产)的配置不同。我习惯把application.yml放到jar包外面,这样发版时不用重新打jar,只要替换配置就行。启动时通过--spring.config.location=/opt/app/user-service/application.yml显式指定外部配置,比在jar里维护多套profile更直观。

第三个是关于备份的。每次发版前把旧jar拷贝一份到/opt/app/user-service/backup/目录,是个成本极低但性价比极高的习惯。一旦新版本启动失败或出现严重问题,可以快速回滚到上一版。配合一个简单的rollback.sh脚本,回滚操作可以控制在1分钟以内。

第四个是关于监控的。服务跑起来只是第一步,它还活着才是真正的目标。Spring Boot Actuator加一个简单的进程监控脚本,或者直接接一个云监控服务,都比等到用户反馈“网站挂了”再排查要强得多。

多模块Maven项目部署到云服务器,本质是一次“从代码到可用服务”的链路打通。核心环节其实就三个:本地能构建出正确的产物、服务器能正确运行产物、未来能方便地更新和回滚。第一条靠POM配置和Maven命令的熟练度,第二条靠Java环境和启动脚本的规范性,第三条靠部署流程的自动化程度。把这三个环节理顺了,后面无论换多少台服务器、加多少个模块,都能做到心里有数,不慌不忙。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询