公司动态
Spring Boot与Vue前后端分离项目一体化Jar包部署实战
1. 项目背景与核心价值最近在社区里看到不少朋友在问Jeecg-boot项目怎么部署尤其是前后端分离后如何把前端Vue项目和后端Spring Boot项目打包成一个可执行的jar包然后一键部署。这确实是个挺实际的需求特别是对于中小型项目或者需要快速交付、简化运维的场景。传统的部署方式前端用Nginx托管dist目录后端单独跑一个jar包虽然清晰但服务器上要维护两个服务配置起来也麻烦。如果能整合成一个jar部署和迁移就方便多了直接java -jar就能启动整个应用。Jeecg-boot本身是一个基于Spring Boot的低代码开发平台它默认的前后端分离架构非常清晰。但官方文档可能更侧重于开发对于生产环境的“一体化”部署讲得没那么细。我结合自己最近在一个内部管理系统上的实践把前后端jar部署的完整流程、关键配置、以及我踩过的几个坑都梳理出来。这个过程不仅适用于Jeecg-boot对于任何采用类似架构Spring Boot后端 Vue/React等独立前端的项目都有参考价值。核心思路就是将前端构建出的静态资源HTML、JS、CSS等嵌入到Spring Boot的jar包中由Spring Boot的内置容器如Tomcat来统一提供静态资源服务和API服务。2. 前端资源整合从独立构建到嵌入Jar包要实现前后端一个jar包部署第一步也是最关键的一步就是处理好前端资源。Jeecg-boot的前端项目通常是一个独立的Vue项目我们需要在构建阶段做一些调整让它的产出物能够被后端项目识别并打包进去。2.1 前端项目构建配置调整首先进入你的前端项目根目录比如jeecg-boot-vue3。核心目标是修改vue.config.jsVue CLI项目或vite.config.jsVite项目中的输出路径。对于Vue CLI项目打开vue.config.js你需要修改outputDir和publicPath这两个配置项。outputDir决定了构建后文件输出的目录我们需要把它指向后端项目的静态资源目录。publicPath则决定了打包后资源文件JS、CSS、图片的引用路径这在嵌入后尤为重要。// vue.config.js const path require(path); module.exports { // 假设你的后端Spring Boot项目根目录是同级目录下的 jeecg-boot // 我们将dist输出到后端的 src/main/resources/static 目录下 outputDir: path.resolve(__dirname, ../jeecg-boot/src/main/resources/static), // 公共路径生产环境设置为根路径 /或者 ./取决于你的部署环境 // 当静态资源被Spring Boot服务时通常设置为 / 或相对路径 ./ publicPath: process.env.NODE_ENV production ? ./ : /, // ... 其他配置 }这里有个关键点publicPath设置为./意味着所有资源如app.123abc.js都会以相对路径被引用例如./js/app.123abc.js。这能确保无论你的应用部署在服务器的哪个上下文路径下比如直接IP访问或者通过Nginx代理到某个子路径前端资源都能被正确加载。如果设置为/则要求应用必须部署在根路径下。对于使用Vite构建的项目配置在vite.config.js中原理类似// vite.config.js import { defineConfig } from vite export default defineConfig({ build: { // 输出目录 outDir: ../jeecg-boot/src/main/resources/static, // 资源路径 assetsDir: assets, }, base: ./, // 相当于 publicPath })配置完成后在前端项目目录下运行构建命令例如npm run build或yarn build。如果一切顺利你会在后端项目的src/main/resources/static目录下看到生成的前端文件如index.html、js、css、assets等文件夹。注意每次前端代码更新后都需要重新执行构建并确保构建输出到了正确的后端资源目录。这是一个容易遗漏的步骤建议在CI/CD流程中自动化。2.2 后端Spring Boot静态资源配置前端资源已经放到了src/main/resources/static目录下。Spring Boot默认就会将这个目录下的内容作为静态资源提供服务。但为了确保万无一失特别是处理前端路由如Vue Router的history模式时我们还需要进行一些配置。首先检查你的application.yml或application.properties配置文件。确保没有禁用或覆盖默认的静态资源处理。通常不需要额外配置。但是为了支持Vue Router的history模式URL中不带#号我们需要一个兜底配置。当用户直接访问一个前端路由如/user/profile时这个路径在后端并没有对应的ControllerSpring Boot会返回404。为了解决这个问题我们需要添加一个配置将所有未匹配API请求的路径都转发到前端的index.html由前端路由来处理。在Spring Boot中可以通过实现WebMvcConfigurer接口并重写addResourceHandlers方法或者更简单地使用一个错误处理配置。这里推荐一个更清晰的方式创建一个简单的Controller。// 例如com.jeecg.config包下创建 FrontEndController.java package com.jeecg.config; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; Controller public class FrontEndController { /** * 匹配所有前端路由路径将其转发到index.html * 注意这个Controller的映射路径不能和你的后端API接口冲突。 * 通常API接口有固定的前缀如 /api所以这里用 /** 是安全的。 */ RequestMapping(value {/, /{path:[^\\.]*}}) public String forward() { return forward:/index.html; } }这个Controller的作用是当请求路径不是以文件扩展名如.js,.css,.jpg结尾时正则[^\\.]*匹配不含点的路径都将其转发到/index.html。这样像/dashboard、/user/list这样的前端路由就能被正确捕获并由前端应用接管。实操心得这个Controller的位置很重要。确保它所在的包能被Spring Boot组件扫描到通常在主启动类同级或子目录下。另外务必确认你的后端API都有统一的前缀如/api、/rest否则像/user这样的路径可能会被这个Controller拦截导致后端接口无法访问。这是整合部署中最容易踩的坑之一。3. Maven打包配置构建一体化可执行Jar当前端资源就位、后端路由配置好后下一步就是通过Maven将整个项目打包成一个可执行的“胖Jar”Fat Jar或“可执行Jar”。这个Jar包内会包含编译后的Java类、所有依赖的第三方库放在BOOT-INF/lib/下、以及我们前端构建的静态资源。3.1 核心插件spring-boot-maven-pluginSpring Boot项目打包的核心是spring-boot-maven-plugin。我们需要在项目的pom.xml文件中确保这个插件被正确配置。build plugins !-- Spring Boot Maven 插件 -- plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version${spring-boot.version}/version !-- 版本与Spring Boot保持一致 -- executions execution goals !-- 这个goal使得package阶段生成可执行jar -- goalrepackage/goal /goals configuration !-- 指定主启动类非常重要 -- mainClasscom.jeecg.JeecgApplication/mainClass /configuration /execution /executions /plugin !-- 可能还需要Maven资源过滤插件确保资源文件被正确处理 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-resources-plugin/artifactId version3.3.0/version configuration encodingUTF-8/encoding /configuration /plugin /plugins /buildgoalrepackage/goal是关键它会在Maven标准的package阶段之后对生成的jar进行重新打包将应用依赖和资源文件整合进去并设置可执行的主类。3.2 处理前端资源maven-clean-plugin 与 maven-resources-plugin这里有一个常见的陷阱如果你先打包后端再构建前端那么后端的jar包里就不会包含最新的前端资源。反之亦然。为了保证每次打包时jar包里的前端资源都是最新的我们需要调整Maven的生命周期确保在打包前前端资源已经被构建并复制到位。一个比较稳妥的做法是将前端构建命令集成到Maven构建过程中。但这需要Node.js环境在纯服务器构建时可能增加复杂度。另一种更简单、更通用的做法是将前端构建作为一个独立的步骤并确保在运行Maven打包命令前前端资源已经存在于src/main/resources/static目录下。为了确保每次打包都是干净的我们可以在pom.xml中配置maven-clean-plugin在清理时也删除旧的静态资源目录。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-clean-plugin/artifactId version3.3.0/version configuration !-- 清理时同时删除静态资源目录避免残留旧文件 -- filesets fileset directorysrc/main/resources/static/directory includes include**/*/include /includes followSymlinksfalse/followSymlinks /fileset /filesets /configuration /plugin然后我们可以使用maven-resources-plugin来“验证”资源是否存在或者通过一个前置的Maven生命周期阶段如generate-resources来触发一个外部脚本如Shell或Node脚本去构建前端。但对于大多数手动部署的场景我建议将前后端构建分离步骤一前端进入前端目录运行npm run build确保输出到后端资源目录。步骤二后端进入后端目录运行Maven打包命令。为了简化你也可以写一个简单的Shell脚本build.sh或批处理文件build.bat来串联这两个步骤。#!/bin/bash # build.sh echo Step 1: Building Frontend... cd ./jeecg-boot-vue3 npm run build if [ $? -ne 0 ]; then echo Frontend build failed! exit 1 fi echo Step 2: Building Backend Jar... cd ../jeecg-boot mvn clean package -DskipTests if [ $? -ne 0 ]; then echo Backend build failed! exit 1 fi echo Build Success! Jar location: target/jeecg-boot-xxx.jar3.3 执行打包与产物分析在命令行中进入后端项目根目录执行mvn clean package -DskipTests参数-DskipTests表示跳过单元测试可以加快打包速度。打包成功后在target目录下会生成两个jar文件jeecg-boot-0.0.1-SNAPSHOT.jar这是重新打包后的、可执行的“胖Jar”。jeecg-boot-0.0.1-SNAPSHOT.jar.original这是Maven标准打包生成的原始jar不包含依赖通常可以忽略。我们可以用jar命令或者解压软件查看“胖Jar”的内部结构# 列出Jar包内容 jar -tf target/jeecg-boot-0.0.1-SNAPSHOT.jar | head -30你会看到类似这样的结构META-INF/ BOOT-INF/ BOOT-INF/classes/ BOOT-INF/classes/application.yml BOOT-INF/classes/static/ BOOT-INF/classes/static/index.html BOOT-INF/classes/static/js/ BOOT-INF/classes/static/css/ BOOT-INF/lib/ BOOT-INF/lib/spring-boot-xxx.jar ... org/BOOT-INF/classes/下面就是你的编译后的Java类文件和资源文件包括我们整合的static目录。BOOT-INF/lib/下面包含了所有项目依赖的jar包。org/目录下是Spring Boot相关的加载器类。这个jar包已经是一个完整的、自包含的应用了。4. 服务器部署与运维实战生成可执行jar后接下来就是把它部署到服务器上运行。这里我们讨论在Linux服务器上的常规部署方式。4.1 基础启动命令与后台运行最基础的启动方式是java -jar jeecg-boot-0.0.1-SNAPSHOT.jar但这会占用当前终端且关闭终端后进程会终止。对于生产环境我们需要让它在后台运行并具备基本的进程管理能力。方案一使用nohup和(最简单)nohup java -jar jeecg-boot-0.0.1-SNAPSHOT.jar app.log 21 nohup让进程忽略挂断信号SIGHUP即使终端关闭也不退出。 app.log将标准输出重定向到app.log文件。21将标准错误2也重定向到标准输出1指向的地方即同一个日志文件。在后台运行。查看日志tail -f app.log查找进程IDps -ef | grep java停止进程先kill [PID]如果不行再用kill -9 [PID]。方案二使用Systemd服务推荐更规范Systemd是Linux系统标准的服务管理器。创建一个服务文件可以方便地启动、停止、重启服务并设置开机自启。创建服务文件sudo vim /etc/systemd/system/jeecg.service写入以下内容根据实际情况修改[Unit] DescriptionJeecg-Boot Application Service Afternetwork.target syslog.target [Service] Typesimple # 修改为你的实际工作目录和jar包路径 WorkingDirectory/opt/jeecg-boot ExecStart/usr/bin/java -jar /opt/jeecg-boot/jeecg-boot-0.0.1-SNAPSHOT.jar # 如果使用外部配置文件可以这样指定 # ExecStart/usr/bin/java -jar -Dspring.config.location/opt/jeecg-boot/config/application-prod.yml /opt/jeecg-boot/jeecg-boot-0.0.1-SNAPSHOT.jar Userwww-data # 建议使用非root用户运行 Groupwww-data Restarton-failure RestartSec10s StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target重新加载systemd配置sudo systemctl daemon-reload启动服务sudo systemctl start jeecg设置开机自启sudo systemctl enable jeecg查看服务状态和日志sudo systemctl status jeecgsudo journalctl -u jeecg -f实时查看日志Systemd方式管理起来更专业日志集成到系统日志中也方便监控。4.2 配置文件外部化与多环境管理在jar包内部写死配置application.yml不利于运维。最佳实践是将配置文件放在jar包外部方便修改而不需要重新打包。Spring Boot支持多种方式指定外部配置文件优先级从高到低命令行参数java -jar app.jar --server.port8081SPRING_APPLICATION_JSON环境变量。java:comp/env中的JNDI属性。Java系统属性-D参数。操作系统环境变量。jar包所在目录的/config子目录下的配置文件。jar包所在目录的配置文件。jar包内部classpath下的/config包。jar包内部classpath根目录。最常用的方式是第6和第7条。假设你的jar包放在/opt/jeecg-boot/你可以这样组织/opt/jeecg-boot/ ├── jeecg-boot-0.0.1-SNAPSHOT.jar ├── application-prod.yml # 优先级次高第7条 └── config/ └── application-prod.yml # 优先级最高第6条启动时Spring Boot会自动加载外部的application-prod.yml。你可以在外部配置文件中覆盖jar包内部的任何配置比如数据库连接、Redis地址、文件上传路径、日志配置等。对于多环境开发、测试、生产可以在外部配置文件中通过spring.profiles.active指定或者更常见的在启动命令中指定java -jar -Dspring.profiles.activeprod jeecg-boot-0.0.1-SNAPSHOT.jar # 或者 java -jar jeecg-boot-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod然后准备多个配置文件application-dev.yml,application-test.yml,application-prod.yml。4.3 JVM参数调优与内存设置对于生产环境根据服务器硬件配置调整JVM参数是必要的这直接影响应用性能和稳定性。一个基础的启动命令可能包含以下参数java -server \ -Xms2g -Xmx2g \ # 堆内存初始和最大设为相同避免动态调整开销 -XX:MetaspaceSize256m -XX:MaxMetaspaceSize256m \ # 元空间大小 -Xmn1g \ # 年轻代大小约为堆的1/2到1/3 -XX:UseG1GC \ # 使用G1垃圾收集器JDK9默认对延迟敏感应用友好 -XX:PrintGCDetails -XX:PrintGCDateStamps -XX:PrintHeapAtGC -Xloggc:/opt/logs/gc.log \ # GC日志 -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/opt/logs/heapdump.hprof \ # OOM时生成堆转储 -jar jeecg-boot-0.0.1-SNAPSHOT.jar \ --spring.profiles.activeprod参数解释与调优建议-Xms和-Xmx设置JVM堆内存的初始大小和最大大小。强烈建议将两者设为相同的值这样可以避免JVM在运行时动态调整堆大小带来的性能开销。具体值取决于你的应用内存需求和服务器总内存。例如在8G内存的服务器上可以设置为-Xms4g -Xmx4g为系统和其他进程留出空间。-XX:MetaspaceSize和-XX:MaxMetaspaceSize元空间Metaspace取代了永久代PermGen的大小。如果应用使用了大量动态类生成如CGlib代理、大量JSP可能需要调大。-Xmn年轻代Young Generation大小。增大年轻代可以减少Minor GC频率但会缩小老年代Old Generation可能增加Full GC频率。通常设置为堆大小的1/3到1/2。-XX:UseG1GC启用G1垃圾收集器。它在高吞吐量和低延迟之间取得了较好的平衡是当前多数场景的推荐选择尤其是JDK 8u40及以上版本。GC日志生产环境务必开启GC日志-Xloggc。这是诊断内存问题、GC停顿时间过长的最重要依据。OOM堆转储开启OOM时的堆转储-XX:HeapDumpOnOutOfMemoryError当发生内存溢出时可以拿到heapdump.hprof文件用MATMemory Analyzer Tool等工具分析内存泄漏点。踩坑记录曾经有一次线上服务在凌晨突然内存飙升后OOM。因为没开GC日志和堆转储排查起来极其困难。后来加上这些参数后再次发生问题时通过分析GC日志发现是频繁Full GC且回收效果差结合堆转储文件最终定位到是一个缓存组件没有设置过期时间导致数据无限增长。所以这些日志和诊断参数就像“黑匣子”平时用不到但出了问题就是救命稻草。5. 部署后的验证、监控与问题排查服务启动后工作还没结束。我们需要验证服务是否正常并建立基本的监控和问题排查能力。5.1 服务健康检查与接口验证Spring Boot Actuator提供了丰富的端点Endpoints用于监控和管理应用。确保在生产配置中启用了健康检查端点。在application-prod.yml中配置management: endpoints: web: exposure: include: health,info,metrics # 按需暴露生产环境通常只暴露health base-path: /actuator # 端点基础路径可以修改以增加安全性 endpoint: health: show-details: when_authorized # 健康详情只在授权时显示启动后访问http://your-server-ip:port/actuator/health应该返回{status:UP}。前端访问验证直接访问服务器IP和端口应该能看到前端页面正常加载。在前端页面中进行登录、查询等操作确认API调用 (/api/等接口) 正常。测试前端路由直接访问一个前端路由地址如http://your-server-ip:port/some/page页面应该能正常显示而不是404。这验证了我们之前配置的FrontEndController是否生效。5.2 日志管理与问题追踪日志是排查线上问题的生命线。Spring Boot默认使用Logback我们可以通过外部配置文件进行详细定制。在application-prod.yml同级目录下可以创建一个logback-spring.xml文件Spring Boot会自动加载它。一个生产环境可用的logback-spring.xml配置示例?xml version1.0 encodingUTF-8? configuration scantrue scanPeriod60 seconds !-- 定义日志文件存储路径 -- property nameLOG_HOME value/opt/logs/jeecg / property nameAPP_NAME valuejeecg-boot / !-- 控制台输出 -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n/pattern charsetUTF-8/charset /encoder /appender !-- 按天滚动的文件日志 -- appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/${APP_NAME}.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy !-- 每天一个文件保留30天 -- fileNamePattern${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory totalSizeCap3GB/totalSizeCap /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n/pattern charsetUTF-8/charset /encoder /appender !-- 错误日志单独输出 -- appender nameERROR_FILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/${APP_NAME}_error.log/file filter classch.qos.logback.classic.filter.ThresholdFilter levelERROR/level /filter rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern${LOG_HOME}/${APP_NAME}_error.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n/pattern charsetUTF-8/charset /encoder /appender !-- 设置日志级别 -- root levelINFO appender-ref refCONSOLE / appender-ref refFILE / appender-ref refERROR_FILE / /root !-- 针对特定包设置更详细的日志级别用于调试 -- logger namecom.jeecg levelDEBUG additivityfalse appender-ref refCONSOLE/ appender-ref refFILE/ /logger !-- 将Hibernate SQL日志输出到单独文件避免主日志文件过大 -- logger nameorg.hibernate.SQL levelDEBUG additivityfalse appender-ref refFILE/ /logger /configuration这个配置实现了日志分级所有日志输出到按天滚动的文件错误日志单独输出到一个文件方便监控。日志滚动每天生成新文件保留30天总大小限制3GB防止磁盘被撑满。控制台输出方便在启动初期查看日志。特定包调试可以针对自己的业务代码包如com.jeecg开启DEBUG级别日志而不影响全局日志量。日志查看命令实时查看最新日志tail -f /opt/logs/jeecg/jeecg-boot.log查看错误日志tail -f /opt/logs/jeecg/jeecg-boot_error.log根据关键字搜索日志grep Exception /opt/logs/jeecg/jeecg-boot.log5.3 常见问题排查思路即使部署成功运行中也可能遇到问题。这里列举几个典型场景和排查思路。问题一前端页面空白或JS/CSS加载404排查打开浏览器开发者工具F12查看Network标签页。看index.html是否成功加载JS、CSS、字体等静态资源是否返回404可能原因及解决前端资源未正确打包进jar用jar -tf your.jar | grep static检查jar包内是否有BOOT-INF/classes/static/目录及文件。如果没有说明前端构建步骤未成功或输出路径错误。publicPath配置错误如果资源路径是http://ip:port/static/js/app.js但实际文件在jar包的根路径下就会404。检查前端构建配置中的publicPath生产环境通常设为./或/并确保与Spring Boot服务的上下文路径匹配。Spring Boot静态资源路径被拦截检查是否有自定义的拦截器Interceptor或过滤器Filter错误地拦截了/static/**或/js/**等路径。可以在拦截器中打印日志或暂时禁用排查。问题二访问前端路由非首页直接返回404排查直接访问http://ip:port/some/route看是返回前端页面还是Spring Boot的404错误页。可能原因及解决FrontEndController未生效或路径冲突确认Controller类被Spring扫描到且其映射路径/**没有和你后端的API接口冲突API应有统一前缀如/api。可以尝试将Controller的映射路径改为更具体的/web/**然后在前端路由配置中也加上/web前缀。Vue Router模式问题如果使用history模式必须确保服务器端这里是Spring Boot能正确转发所有非静态资源的请求到index.html。我们的Controller就是做这个的。如果还有问题可以尝试暂时改用hash模式URL带#测试。问题三服务启动失败端口被占用或数据库连不上排查查看启动日志通常会有明确的错误信息。端口占用netstat -tlnp | grep :8080查看端口占用情况kill掉占用进程或修改server.port配置。数据库连接失败检查application-prod.yml中的数据库URL、用户名、密码。确保数据库服务已启动且防火墙规则允许应用服务器访问数据库端口。问题四服务运行一段时间后内存占用过高或OOM排查首先查看GC日志如果配置了-Xloggc分析GC频率和耗时。频繁的Full GC且每次回收后堆内存下降不多很可能存在内存泄漏。使用jstat -gcutil [pid] 1000命令实时查看GC情况。如果发生OOM且生成了堆转储文件heapdump.hprof使用MAT或JVisualVM加载分析查看占据内存最大的对象是什么以及其引用链。常见内存泄漏点无界缓存本地缓存如HashMap、Caffeine未设置大小限制或过期策略。静态集合类静态的Map、List不断添加对象从未清理。线程局部变量ThreadLocal未清理特别是在使用线程池时ThreadLocal变量在线程复用后可能残留。数据库连接、文件流未关闭虽然现代框架通常有自动关闭机制但在自定义代码中仍需注意。部署一个一体化的Jar包看似只是简单的java -jar但背后涉及到构建流程的整合、配置的管理、运行时的调优和问题的排查。把这些环节都打通并形成规范才能真正实现高效、稳定的部署。对于Jeecg-boot这类项目一体化部署极大地简化了运维复杂度特别适合需要快速部署和交付的场景。