昨天我刚把一个 Spring Boot 项目从本地一路部署到服务器,顺手把整个环境准备过程整理了一遍。这活儿看起来简单,实际上版本选型、环境变量、镜像配置、进程守护,哪个环节没处理好都能卡住你好几个小时。这篇就把 Spring Boot 实战系列的第一篇写透,从零开始把环境搭好、项目跑起来,分本地开发和服务器部署两条线走,每个步骤都附上我踩过的坑和为什么这么做的理由。
1. 这个系列打算怎么做,为什么先从环境讲起
做 Spring Boot 项目实战,最忌讳的就是上来就写代码。你连 JDK 版本都没定下来,写到后面会发现很多依赖不兼容,回头再改环境和项目配置,那真是牵一发动全身。所以系列第一篇,我刻意压住节奏,专门讲环境准备和服务运行。这一步走稳了,后面写接口、接数据库、做权限、上缓存,都是在同一个地基上盖楼,不会返工。
这篇文章适合两类人。一类是刚学完 Java 语法、想正经做一个完整项目的同学,你不知道 JDK 用 8 还是 17、Maven 仓库为什么下载那么慢、IDEA 里那一堆配置到底怎么填。另一类是项目已经写好、正准备扔到服务器上去跑的同学,你对 Linux 上怎么部署 jar 包、怎么让它开机自启、崩了怎么自动重启还不太熟悉。这两类需求,这篇文章都覆盖。
我选择的方案是一条最主流、最稳的路线:JDK 17 + Maven 3.9 + IDEA 社区版 + Spring Boot 3.2,服务器用 CentOS 7 系,部署方式用 systemd 守护 jar 包,数据库暂时不接,第一篇文章先让服务能稳定跑起来。这套组合的优势在于:JDK 17 是目前 Spring Boot 3.x 的推荐基线,Maven 3.9 兼容性稳定,而 systemd 是 Linux 上最标准的进程管理方案。你以后在公司里做项目,大概率也是这个套路。
2. 本地环境准备:JDK 和 Maven 是地基
2.1 JDK 版本怎么选,为什么我不建议用 Java 8
好多初学者对 Java 8 有执念,觉得自己学校上课用的就是 8,网上老教程也全是 8,用它准没错。这个说法放到 2024 年,真该改改了。Spring Boot 3.x 官方要求 Java 17 起步,你要是强行用 Java 8 跑去跑 Boot 3 的项目,启动直接报错,连编译都过不去。就算你坚持用 Spring Boot 2.7 配 Java 8,那也意味着你将来要升级 Boot 3 时,整个项目得动一遍,成本很高。
选 JDK 17 还有一个现实原因:17 是 LTS 长期支持版本,Oracle 和开源社区都会持续维护好几年,性能和稳定性都有保障。你不用担心学完之后技术上就过时了。我个人就是用的 JDK 17 跑项目,实测下来内存占用比 8 合理,GC 表现也更好,特别是写微服务之后,这个优势会更明显。至于 JDK 21,那是新一代 LTS,更激进,但生态适配还需要时间,暂时不急。
2.2 JDK 安装与配环境变量,这一步别偷懒
安装 JDK 的方式在 Windows 和 macOS 上不太一样,但核心逻辑是一回事:装好之后必须配JAVA_HOME和PATH。我见过太多人装完 JDK 命令一敲java -version能显示版本,就以为自己配好了,结果 Maven 一跑就报找不到 JDK。原因就是没配JAVA_HOME,很多工具不认PATH里的 java,只认这个专属变量。
具体步骤,Windows 用户去下载 OpenJDK 17 的 Windows x64 安装包,安装时记住安装路径,比如C:\Program Files\Java\jdk-17。然后打开系统属性里的环境变量,新建一个JAVA_HOME,值填上述路径,再在Path变量里追加一行%JAVA_HOME%\bin。macOS 用户建议直接用 Homebrew:brew install openjdk@17,装完执行echo 'export PATH="/usr/local/opt/openjdk@17/bin:$PATH"' >> ~/.zshrc,再执行echo 'export JAVA_HOME="/usr/local/opt/openjdk@17"' >> ~/.zshrc。
验证是否配置成功,不要只看java -version,还要敲一句echo $JAVA_HOME(Windows 是echo %JAVA_HOME%),能正确打印路径才算真的配好了。这里有个小细节,Windows 上每次改完环境变量,必须重新打开终端窗口才生效,有些人改完忘了重开,卡在这一步半天,纯属冤枉。
2.3 Maven 安装与镜像配置,国内开发者的必修课
Maven 是 Java 项目的依赖管理和构建工具,简单说就是帮你自动下载第三方库、编译代码、打包成可运行文件的工具。Spring Boot 项目里,你只要在pom.xml里声明需要哪些依赖,Maven 就会自动把对应 jar 包拉下来。没有它,你手动找 jar、手动配 classpath,那个工作量能让人崩溃。
Maven 安装本身不复杂,下载二进制包解压,配MAVEN_HOME和PATH。真正的痛点在于:默认中央仓库在国外,国内网络下载一个依赖可能要卡几分钟甚至超时。解决办法是修改 Maven 安装目录下conf/settings.xml,在<mirrors>节点里加一个阿里云镜像。
我贴一下我一直在用的镜像配置:
<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>这个mirrorOf填*,意思是所有中央仓库的请求都走这个镜像。配完之后,Maven 下载依赖的速度会从几 KB/s 变成几 MB/s,体感差距巨大。还有一个小建议:Maven 默认的本地仓库在用户目录下的.m2文件夹,如果你 C 盘空间紧张,可以在settings.xml里用<localRepository>指定到别的盘。
2.4 IDE 选择与配置,IDEA 的设置技巧
IDE 我推荐用 IntelliJ IDEA。社区版免费,Spring Boot 开发已经够用了。装好之后有几个地方建议预先设置。第一是让 IDEA 使用你自己装的 JDK 而不是内置的,在 File > Project Structure > SDK 里添加你刚才配的 JDK 17。第二是配置 Maven 的 settings 文件,让 IDEA 读取你改过的settings.xml,这样 IDEA 里跑 Maven 命令也能享受镜像加速。
第三是个细节但很影响体验:打开 IDEA 的 Settings,搜索auto-import,把 Add unambiguous imports on the fly 勾上。这样你写代码时 IDEA 会自动帮你导入需要的类,不用手动一个个 Alt+Enter。另外建议把Build project automatically勾上,改完代码热编译会快很多。
3. 第一个 Spring Boot 项目创建与本地运行
3.1 创建项目,两种方式随你挑
创建 Spring Boot 项目有两条路。一条是用 IDEA 自带的 Spring Initializr 向导,File > New > Project > Spring Boot,然后填项目坐标、选 JDK 17、选依赖。另一条是直接上 start.spring.io 网站,网页上把配置填好,下载一个 zip 包,解压后用 IDEA 打开。两种方式本质一样,都是生成一个标准骨架。
我建议第一次做项目的人直接用 IDEA 向导,因为能省去创建后手动导入 Maven 项目的步骤。Group 填你公司的域名倒写,比如com.example,Artifact 填项目名,比如demo。依赖这边,第一篇先不勾 Web 之外的选项,一个Spring Web就够把服务跑起来。
3.2 看一遍生成出来的目录结构,别再视而不见
项目生成之后,那一堆目录和文件不是摆设。src/main/java下面放着你的 Java 源码,src/main/resources放着配置文件,pom.xml是 Maven 的依赖清单。还有一个关键类,名字是项目名+Application,比如DemoApplication.java,里面只有一个main方法,它是整个项目的启动入口。
Spring Boot 最迷人的地方就是"约定优于配置"。你不需要像老 Spring 那样写一堆 XML 配置,只要启动入口类在包的顶层,它就能自动扫描到所有子包里的组件。我见过新手把启动类放错位置,放到某个子包下面,结果 Controller 死活扫描不到,接口 404。这个坑很经典,等会儿我在常见问题里专门讲。
3.3 本地跑起来,写个最朴素的接口试试
项目生成后,直接在 IDEA 里运行DemoApplication的main方法,看控制台输出。出现 Spring Boot 的 banner 和Tomcat started on port(s): 8080就说明启动成功了。现在项目里还没有任何接口,访问http://localhost:8080会看到错误页,这很正常。
我们来写一个最基础的接口验证全链路。在src/main/java里建一个controller包,新建类HelloController.java:
package com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello, Spring Boot!"; } }重新启动项目,浏览器访问http://localhost:8080/hello,能看到Hello, Spring Boot!就说明本地环境完全打通了。这一步是整个系列的"点火",从空的骨架到一个能响应的服务,你会第一次感受到 Spring Boot 这套开发模式有多丝滑。
4. 服务器环境准备,让服务在 Linux 上跑起来
4.1 服务器选型与基础配置,买一台机器之后先做什么
本地跑通只是第一步,项目最终要部署到服务器上才能给别人用。服务器我建议直接选主流的云服务器厂商,系统镜像选 CentOS 7.9 或者 Ubuntu 20.04 都行。新手推荐 CentOS,因为网上资料多,遇到问题好搜。配置方面,1 核 2G 起步,内存再小的话编译打包会很吃力。
拿到服务器第一件事,建议先创建一个专门的部署用户,不要直接用 root 跑应用。用 root 图省事一时爽,万一应用被入侵,整个服务器直接裸奔。我常用的做法是:
useradd -m -s /bin/bash deploy passwd deploy然后给这个用户添加 sudo 权限:visudo,在root ALL=(ALL) ALL下面加一行deploy ALL=(ALL) ALL。之后所有部署操作都用这个普通用户执行,必要提权时才加 sudo。
4.2 服务器上安装 JDK,这次不配环境变量了?
服务器上装 JDK 方式跟本地差不多,但有个更省事的选择:用包管理器直接安装。CentOS 上执行yum install java-17-openjdk,Ubuntu 上执行apt install openjdk-17-jdk。包管理器装的 JDK 有一个好处:系统会自动把它放入 PATH,不用手动配环境变量,省掉一个出错的环节。
但要注意,用包管理器装完 JDK 之后,系统里可能同时存在多个 Java 版本。这时候要确认当前默认的 java 指向哪个版本,执行java -version看一眼,如果版本不对,用alternatives --config java切换一下(CentOS 系)。Ubuntu 上对应的是update-alternatives --config java。这个细节容易忽略,但项目一旦编译报版本错误,排查时兜一大圈才发现是这个问题。
4.3 打包上传,jar 包是最终的交付物
本地代码开发好后,部署到服务器的第一件事是打包。在 IDEA 的终端里进入项目根目录,执行:
mvn clean package -DskipTests这个命令会先清理之前的编译产物,然后重新编译、打包,-DskipTests表示跳过测试用例执行(但会编译测试代码),加快打包速度。打包完成后,target目录下面会生成一个demo-0.0.1-SNAPSHOT.jar。这就是我们要上传到服务器的那个文件。
上传文件的方式有很多,scp 命令是最直接的:
scp target/demo-0.0.1-SNAPSHOT.jar deploy@服务器IP:/home/deploy/Windows 上没有 scp 的话,用 WinSCP 或者 IDEA 自带的 Deployment 功能上传也行。上传之后,先手动跑一次,确认代码在服务器上的 JDK 和环境下能正常启动:
java -jar demo-0.0.1-SNAPSHOT.jar看到和本地一样的启动日志,说明服务器的 JDK、网络配置都没问题。这时候你就可以执行Ctrl+C停掉它,进入下一步的系统化部署。
4.4 systemd 配置,让服务有"守护神"
java -jar直接启动有一个致命问题:终端一关,进程就没了。而且进程只要崩溃,不会自动恢复。想让服务稳定运行、开机自启、崩了自动重启,就要用 Linux 的 systemd 服务来托管。
在/etc/systemd/system/下创建一个服务文件,我给它起名demo.service:
[Unit] Description=Spring Boot Demo Application After=network.target [Service] User=deploy ExecStart=/usr/bin/java -jar /home/deploy/demo-0.0.1-SNAPSHOT.jar SuccessExitStatus=143 Restart=always RestartSec=10 [Install] WantedBy=multi-user.targetExecStart是你启动 jar 包的命令,路径必须写全,java 的可执行文件路径可以用which java查一下。Restart=always是精髓,进程无论因为什么原因退出都会自动拉起,间隔 10 秒重试一次。SuccessExitStatus=143是专门处理systemctl stop的:Java 应用收到终止信号退出时状态码是 143,如果不加这一行,systemd 会认为服务是异常退出,从而错误地触发自动重启。
配置好之后,依次执行:
systemctl daemon-reload systemctl start demo systemctl status demo systemctl enable demoenable是设置开机自启。这四步执行完,你的 Spring Boot 服务就真正在服务器上扎下根了。访问http://服务器IP:8080/hello,能正常收到响应,就说明整个部署链路完全跑通。
4.5 开放端口和安全组,连不上别慌
服务器部署之后你发现外网访问不了,十有八九不是代码问题,而是端口没开。云服务器厂商的安全组规则,和 Linux 系统自带的防火墙,是两个独立关卡,得都放行才行。安全组在云控制台操作:找到实例,进入安全组配置,添加一条入方向规则,协议 TCP,端口填 8080,来源可以填 0.0.0.0/0(表示允许所有来源访问)。
Linux 防火墙这层,如果服务器是 CentOS 7,执行:
firewall-cmd --zone=public --add-port=8080/tcp --permanent firewall-cmd --reload如果执行firewall-cmd提示未安装,说明系统默认没启用防火墙服务,那这层就不用管。Ubuntu 上一般用的 ufw,操作是ufw allow 8080/tcp。这个双关卡问题我见过太多次了,明明服务在服务器本地 curl 都通,外面访问就是不行,结果查了半天发现安全组没放行。
5. 功能验证与压测,别只看"能启动"就完事
5.1 从"能启动"到"能用",至少验证这三个维度
服务启动成功只是最底层的门槛。我习惯把部署后的验证分成三个维度。第一个维度是进程与端口状态:systemctl status demo和netstat -tlnp | grep 8080确认服务和端口都在。第二个维度是接口可达性:本地curl http://localhost:8080/hello,外网从你自己电脑上浏览器访问服务器 IP 加端口。第三个维度是日志有无异常:journalctl -u demo -f实时查看服务日志,确认没有堆栈异常。
这三个维度全部通过,才叫真正的部署完成。中间任何一个维度出问题,排查逻辑都会不一样,后面会专门讲。
5.2 用 ab 做一个简单的并发冒烟测试
部署完成之后,我建议顺手做一个轻量级的压测,目的不是为了测性能上限,而是验证服务在并发请求下会不会崩。Apache 自带的 ab 工具就够用:
ab -n 1000 -c 100 http://服务器IP:8080/hello这行命令表示总共发送 1000 个请求,每次并发 100 个。跑完看两个指标:Failed Requests应该为 0,Requests per second是吞吐率。1 核 2G 的服务器跑这种空接口,每秒处理几百个请求完全没问题。如果出现大量失败,先别骂代码,检查一下是不是系统文件句柄数太低了,执行ulimit -n看看,通常要改到 65535 才够用。
还有一个容易踩的坑:云服务器默认对入方向流量有一定限制,压测跑出来的数字仅供参考,不要拿它做绝对的性能判断。真要严格压测,得在同机房内网用专门的压测机。
6. 常见问题速查表,每个坑都是真金白银换来的
我把这一路实操中真正遇到过的经典问题整理成一张表,按排查顺序排列:
| 现象 | 常见原因 | 排查与解决 |
|---|---|---|
java -jar提示找不到主类 | 打包时没包含启动类,或 pom 里缺失 spring-boot-maven-plugin | 检查 pom.xml 是否声明了 spring-boot-maven-plugin 插件,重新mvn clean package |
| 接口访问 404,但服务启动成功 | 启动类不在包的最外层,组件扫描不到 Controller | 把启动类移到com.example.demo根包下,Controller 放在子包中 |
| 服务启动报端口占用 | 8080 端口被其他进程占了 | lsof -i:8080(macOS/Linux)或 `netstat -ano |
| 修改配置后重启不生效 | 进程没被真正杀掉,旧进程还活着 | `ps -ef |
| 外网无法访问,但本地 curl 通 | 云安全组或系统防火墙没放行端口 | 先在服务器上curl localhost:8080,通了就检查安全组和防火墙规则 |
| systemctl 只能看到 running,但服务一访问就挂掉 | 内存不足被 OOM Killer 干掉 | 执行 `dmesg |
| 服务器日志显示时区差 8 小时 | 系统时区不是 Asia/Shanghai | 用timedatectl set-timezone Asia/Shanghai修改系统时区,或者在启动命令加-Duser.timezone=Asia/Shanghai |
6.1 端口占用这个"老朋友"值得单独聊五块钱
端口占用是新手最容易碰到的第一个拦路虎。在本地还好,报错信息会直接告诉你端口被占用了,但有时候你明明已经停止了一个项目,再次启动还是报端口占用。原因很简单:IDEA 停止运行不算真正杀掉进程,经常会有残留的 Java 进程还占着端口。
处理办法是找到这个进程然后杀掉。Windows 用户执行netstat -ano | findstr 8080,能看到占用端口进程的 PID,再执行taskkill /F /PID 这里填PID。macOS 和 Linux 用户执行lsof -i:8080找到 PID,然后kill -9 PID。我自己的习惯是,每次改完配置要重启服务之前,都会顺手检查一下端口,防止旧进程残留导致的新旧端口冲突。
6.2 内存不足导致的进程被杀,最难查的问题之一
因为内存不足导致服务进程消失的情况,在 1 核 2G 的服务器上很常见。表面上 systemctl 显示服务 stopped,你查看日志又找不到明显的 Java 异常,这就很迷了。真正的元凶是 Linux 的 OOM Killer,系统内存不够时会强制杀掉一些进程来保命。
排查命令是dmesg | grep -i killed,能看到类似Out of memory: Kill process 12345 (java)的记录。解决方案一是加物理内存,最直接;二是在启动命令里给 JVM 限制堆大小,避免它默认吃掉过多内存。给部署服务增加内存限制,比如:
ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /home/deploy/demo-0.0.1-SNAPSHOT.jar-Xms是初始堆大小,-Xmx是最大堆大小。在 2G 总内存的服务器上,给 JVM 限制到 512m,再给系统其他程序留足余地,是最稳的配置。这个参数对线上服务的影响极大,在容器部署那篇还会再展开讲。
7. 关于这套流程,最后分享一点我自己的体会
这套"本地开发 + systemd 托管"的部署方案,我用了好几年。刚开始玩服务器那会儿,我也是老老实实java -jar一把梭,后来有一次服务半夜挂了没人管,第二天早上才知道,从那之后就老老实实给每个项目都配上 systemd 守护。很多人觉得 systemd 配置有学习成本,但对比一下:手动部署的进程管理全靠记性,崩溃恢复全靠运气,而 systemd 把这个过程变成了声明式的配置,一劳永逸。这个投入产出比,怎么算都划算。
再有就是环境准备这件事,真的值得在第一篇就做扎实。JDK 版本、Maven 镜像、IDEA 设置、服务器文件句柄、时区配置,这些基础工作像下水管道,平时不会注意,一旦出问题就是大麻烦。你把这篇文章里的每一步都亲手敲一遍、跑通一遍,后面写业务代码的时候,你就能把全部精力放在代码本身,而不是老纠结环境问题。下一篇我会在这个环境基础上,把数据库接进来,做一个带增删改查的完整接口,到时候你就知道今天这些准备有多值了。