最近不少朋友在搭新项目的时候都来问我同一个问题:Spring Boot 3.x 都出了这么久了,有没有一套真正能直接上生产的单体脚手架,别整那些花里胡哨的微服务全家桶。所以我一口气整理了这套基于JDK17 + Spring Boot 3.x + Nacos + JWT + Docker的生产级单体脚手架搭建全过程,从环境准备、Nacos 接入、JWT 认证到 Docker 部署,每个环节都给出可落地的代码和踩坑记录,新手能照着一步步做,老手也能拿来当 checklist 用。
这套方案解决的是大多数中小型项目的真实痛点:既要 Spring Boot 3.x 的新特性和 JDK17 的性能提升,又不想一上来就被微服务的复杂度拖垮。单体架构起步,保留 Nacos 做注册和配置中心给未来拆分留后路,JWT 做无状态认证减少 session 管理负担,Docker 一键部署降低环境差异带来的交付成本。适合想从零搭一个能扛住生产流量、又不用过度设计的后端项目的开发者参考。
1. 整体设计与技术选型思路
1.1 为什么是 JDK17 而不是 JDK8 或 JDK21
Spring Boot 3.x 官方的最低要求就是 JDK17,这一点直接就帮我们做了选择题。JDK8 虽然存量项目多,但 Spring Boot 3.x 完全不兼容 JDK8,强行用老 JDK 就意味着只能停留在 Spring Boot 2.x,很多新特性用不上。
JDK17 本身是 LTS 长期支持版本,Oracle 和 OpenJDK 社区都承诺了至少到 2029 年的维护周期,安全补丁跟得上,生产环境不用担心停更问题。对比 JDK21 来说,17 经过两年多的生态验证,各种中间件、字节码增强工具、第三方库的兼容性已经非常成熟了。加上 Nacos、Sentinel 等阿里系中间件对 JDK17 的适配也早就稳定,踩坑成本最低。
这里多说一句 JVM 参数调整。很多从 JDK8 迁移上来的项目会在启动脚本里带着-XX:PermSize这类参数,JDK17 里 Perm 区早被 Metaspace 取代了,这些参数直接不识别,启动就报错。我习惯的统一配置是-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m,单体应用按这个规格起步,根据实际压测再调。
1.2 单体架构与 Nacos 的组合逻辑
现在很多团队有个误区,觉得上了 Spring Cloud 就必须拆微服务。实际上对于绝大多数业务团队来说,单体架构才是起步阶段的正确选择,尤其是用户量还没到千万级、团队规模在十人以下的场景。我的经验是:能用单体解决的问题,就不要制造分布式问题。分布式事务、链路追踪、服务治理这些复杂度,不是小团队该背的负担。
这里选择 Nacos 不是因为要做微服务,而是看中它的注册中心和配置中心两大能力。注册中心在单体阶段最大的价值是让方便的管理端能够发现服务实例,后续如果真的要拆分,服务和配置的接入方式不用改。配置中心则是真金白银的收益——所有环境相关的配置集中管理、支持动态刷新、支持命名空间隔离,比躺在 application.yml 里强太多。
脚手架里我把配置分成了三类:spring.cloud.nacos.discovery负责注册发现,spring.cloud.nacos.config负责配置中心,业务配置通过@ConfigurationProperties配合@RefreshScope实现动态更新。这样从单体向微服务演进的时候,业务代码几乎不用动。
1.3 认证方案选型:JWT 与 Session 怎么选
JWT 无状态这个特性被很多人误解,觉得服务端不存登录状态就是无敌的。实际上 JWT 的退出登录问题、令牌续签问题、密钥管理问题都需要认真设计,不存在银弹。我选择 JWT 的核心原因是为了让认证信息可以跨服务共享,方便后续拆分,以及在网关层做统一鉴权时不需要查库。
具体设计上,我采用了 access token + refresh token 的双令牌方案。access token 有效期设短一些,一般 30 分钟到 2 小时,refresh token 有效期设长一些,比如 7 天。这样即使 access token 泄露,攻击者利用窗口期也短;用户退出登录时虽然无法真正吊销 access token,但可以通过黑名单机制把 refresh token 拉黑,实现近似注销效果。
签名算法我用了 HS256,配合一个足够长的随机密钥(至少 32 字节)。如果你有跨服务验证和未来拆分网关的需求,可以升级成 RS256,用非对称公私钥做签名和验签,但密钥管理成本会高一些,单体阶段 HS256 完全够用。
2. 环境准备与脚手架基础搭建
2.1 JDK17 安装与验证
不管你是 Windows、Linux 还是 macOS,第一步都是装 JDK17。Windows 用户最省事的方式是下载官方安装包,直接下一步安装,或者用解压版配置环境变量。Linux 服务器上我习惯用 tar 包解压安装,因为这样路径可控,不污染系统包管理器。
安装完之后一定要在命令行里验证:
java -version javac -version两个命令如果都能输出17.x.x版本信息,环境变量就配好了。常见问题是只配了JAVA_HOME没把$JAVA_HOME/bin加到PATH里,导致java能执行但javac找不到,这类问题先检查echo $JAVA_HOME再检查 PATH,优先级顺序通常是:系统 PATH > 用户 PATH。
IDEA 里如果用的是社区版,新建项目时选 Spring Initializr 仍然可以正常创建 Spring Boot 3.x 项目,社区版和旗舰版在项目创建、代码编写、热部署这些核心功能上没有差别,只是少了 Spring 相关的可视化辅助面板,不影响学习使用。JDK 也要在 Project Structure 里指定成 17,否则编译会失败。
2.2 快速初始化 Spring Boot 3.x 工程
工程创建有两种路子:一种是去 Spring Initializr 网页选好依赖生成压缩包,另一种是在 IDEA 里直接生成。推荐用 Initializr,把项目元信息填好,依赖勾上Web、Validation、MyBatis、MySQL Driver、Lombok这几个基础项,版本选3.3.x或者3.4.x稳定版。
生成完工程后第一件事是改 Maven 的仓库配置,把默认的中央仓库镜像换成国内源,不然打包的时候能急死人。在~/.m2/settings.xml里配一个阿里的镜像就行,注意 Spring Boot 3.x 相关依赖在阿里云的 maven 镜像里都已经同步了,不用担心版本拉不到的问题。
pom.xml里除了初始化的依赖,还需要手动补上 Nacos 和 JWT 相关的依赖。Nacos 如果做配置中心和注册中心,要加的是spring-cloud-starter-alibaba-nacos-discovery和spring-cloud-starter-alibaba-nacos-config两个包,注意版本必须和 Spring Cloud 版本对得上。我在 Spring Boot 3.3.x 下用的是 Spring Cloud Alibaba 2023.0.3.x,对应的 Nacos client 版本在 2.3.x 左右,这些对应关系在 Spring Cloud Alibaba 官方文档的版本说明页有明确表格,照着选就不会错。
JWT 库我用的是io.jsonwebtoken:jjwt-api / jjwt-impl / jjwt-jackson0.12.x 版本,把三个都加上,这个库是最主流的 Java JWT 实现,API 稳定,文档也多。
2.3 统一响应体与全局异常处理
跑业务的脚手架必须先把返回格式统一了,否则前后端联调时各写各的,接口文档都对不上。我用的统一响应体:
public class Result<T> { private int code; private String message; private T data; // 静态工厂方法:success() / error() }code 约定:0代表成功,业务异常用正数区分,系统异常用负数。别用 HTTP 状态码直接当业务码,两者含义不同,HTTP 状态码是给网络层看的,业务码是给前端逻辑判断用的,混在一起很容易出问题。
全局异常处理用@RestControllerAdvice统一拦截,关键点是分三类:参数校验异常(MethodArgumentNotValidException)、业务异常(自定义BizException)、系统异常(兜底的Exception)。每一类都返回固定结构的 Result,同时打印日志把异常堆栈留在服务端,避免把内部错误信息直接暴露给前端。
2.4 统一日志与链路标识
单体应用虽然不需要全链路追踪,但统一日志格式必须从第一天就做好,否则出了线上问题查日志那叫一个痛苦。logback 配置里我固定了三个字段:时间戳、日志级别、traceId。traceId 是每个请求入口生成的 UUID 短串,通过 MDC 放进日志上下文,这样哪怕并发请求混在一起,也能按 traceId 把一次请求涉及的所有日志串起来看。
实现上不用引入复杂框架,一个简单的OncePerRequestFilter就够:从 Header 里取上游透传的 traceId,没有就自己生成,set 进 MDC,请求结束 remove。实测这个方案在单体阶段非常够用,以后接 SkyWalking 或 Zipkin 再替换不迟,脚手架留好这个位置就行。
3. Nacos 注册中心与配置中心接入
3.1 通过 Docker 快速部署 Nacos 单机版
Nacos 本身是个 Java 应用,最省事的部署方式就是 Docker。我生产环境用的 2.5.4 版本,单机模式部署命令:
docker run -d \ --name nacos \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ -e MODE=standalone \ -e NACOS_AUTH_ENABLE=true \ -e NACOS_AUTH_TOKEN=SecretKey012345678901234567890123456789012345678901234567890123456789 \ -v /data/nacos:/home/nacos/data \ -v /data/nacos/logs:/home/nacos/logs \ nacos/nacos-server:v2.5.4这里有几个关键点。9848端口是 Nacos 2.x 新增的 gRPC 端口,1.x 只有 8848 一个客户端访问端口,2.x 必须把 9848 也暴露出来,否则客户端会报连接超时。NACOS_AUTH_ENABLE=true是必须开的,上一版 Nacos 默认没开鉴权,直接导致一堆人搭完发现任意请求都能访问控制台读配置,这个安全隐患踩过的都知道有多坑。
数据目录挂载出来,容器删了配置还在,这个习惯一定要养成。另外 Nacos 官方有镜像长期维护,版本选择建议用 2.x 的稳定版本,别用最新的发版就升级。
3.2 数据库存储与持久化配置
Nacos 默认自带内嵌数据库,看了不少教程的人以为这就是个普通的配置存储,实际上内嵌数据库一旦容器重建,所有配置和服务实例信息全没了。生产环境必须切到外部数据库存储,这也是一个容易踩的坑。
先建库执行初始化脚本,Nacos 解压包或者 GitHub 仓库里能找到nacos-mysql.sql,执行完后配置数据源,上述 Docker 命令里加环境变量即可:
-e SPRING_DATASOURCE_PLATFORM=mysql \ -e MYSQL_SERVICE_HOST=xxx \ -e MYSQL_SERVICE_DB_NAME=nacos_config \ -e MYSQL_SERVICE_USER=xxx \ -e MYSQL_SERVICE_PASSWORD=xxx \这里要注意一点:如果公司的数据库是国产数据库比如达梦,Nacos 2.5.4 也是支持的,连接达梦的配置在 Nacos 的官方文档里有对应的数据源适配说明,需要额外改一下 Nacos 的application.properties,把驱动和方言替换掉。核心思路还是先确保本地开发环境能跑通 Nacos,再把生产的数据源切换过去。
3.3 Spring Boot 接入注册中心
application.yml里加服务发现配置:
spring: application: name: demo-server cloud: nacos: server-addr: 127.0.0.1:8848 discovery: namespace: dev group: DEFAULT_GROUP username: nacos password: nacos关键点在于 namespace 的用法。我推荐按环境分 namespace:dev、test、prod各一个。这是因为 Nacos 的配置是按 namespace 隔离的,服务注册也可以指定 namespace。你在 dev 环境注册的服务实例,在 test 环境是看不到的,真正做到环境隔离。很多人一开始不分 namespace,结果 dev 和 test 共用一套 Nacos,互相能看到服务,调试的时候各种串环境,非常痛苦。
启动项目后去 Nacos 控制台的「服务管理—服务列表」页面里确认服务是否注册成功。如果页面上能看到实例列表,注册中心这就通了。
3.4 配置中心接入与动态刷新
配置中心这步是脚手架的分水岭,很多教程只做了注册,没做配置管理,等于只用了一半功能。Nacos 配置中心的核心优势是:配置改动不需要重启应用,实时推送到客户端。
我建议把数据库连接、Redis 地址、第三方密钥、限流阈值这些环境相关的配置都挪到 Nacos 配置中心里。业务上用@RefreshScope标注要让配置热更新的 Bean,配置变化时重新加载。
具体配置:
spring: cloud: nacos: config: server-addr: 127.0.0.1:8848 namespace: dev group: DEFAULT_GROUP file-extension: yaml配置中心的 dataId 命名遵循${spring.application.name}-${profile}.${file-extension}的规则,比如demo-server-dev.yaml。在 Nacos 控制台的「配置管理—配置列表」里创建这个配置,Spring Boot 应用启动时就会自动拉取。
动态刷新的关键步骤:
- 在 Nacos 配置中心创建
demo-server-dev.yaml配置 - 本地
application.yml里保留最基础的启动配置,环境相关配置全部下沉到 Nacos - 需要热更新的 Bean 上加
@RefreshScope - 修改 Nacos 配置后点发布,应用自动更新,不用重启
注意@RefreshScope不是万能的,它只能刷新被它托管的 Bean。如果是@ConfigurationProperties配置类属性变化,需要配合@RefreshScope标注在这个配置类上,否则配置类不重建、属性不刷新,这是最容易踩的坑之一。
4. JWT 认证授权体系实现
4.1 令牌生成与校验核心代码
JWT 的核心组件是三段式结构:Header、Payload、Signature。Header 里声明签名算法,Payload 里放业务声明数据,Signature 是防篡改的签名。我用 jjwt 0.12.x 实现的生成和解析方法:
@Component public class JwtTokenProvider { @Value("${jwt.secret-base64}") private String secretBase64; @Value("${jwt.access-token-expire-seconds:1800}") private long accessTokenExpireSeconds; public String generateAccessToken(Long userId, String username, List<String> roles) { byte[] keyBytes = Decoders.BASE64.decode(secretBase64); Date now = new Date(); Date expiry = new Date(now.getTime() + accessTokenExpireSeconds * 1000); return Jwts.builder() .subject(String.valueOf(userId)) .claim("username", username) .claim("roles", roles) .issuedAt(now) .expiration(expiry) .signWith(Keys.hmacShaKeyFor(keyBytes), Jwts.SIG.HS256) .compact(); } }注意secret-base64要配成 Base64 编码后的字符串,不能用明文短字符串,否则Keys.hmacShaKeyFor会因为密钥强度不足直接抛异常。这是很多新手拿到示例代码后最容易踩的坑。
校验工具有两类需求:一类是 Spring Security 体系内的过滤器,另一类是简单拦截器模式。如果你的项目用了 Spring Security 5.7+ 的组件注册方式,我建议直接写一个 OncePerRequestFilter 加进 SecurityFilterChain,核心逻辑就是:取 Authorization Header → 校验令牌 → 把用户信息塞进 SecurityContext。
如果项目没上 Spring Security 全家桶,可以在 WebMvc 拦截器里做校验,逻辑更轻。脚手架项目我推荐后者,因为不用引入一整套 Security 配置的复杂度,一个拦截器加一个注解就能控制接口权限。
4.2 登录接口与用户信息刷新
用户登录接口的处理流程:
- 接收用户名密码,调用
UserDetailsService校验 - 校验通过后生成 access token 和 refresh token
- 把 refresh token 存储到 Redis(key 是
login:refresh:{userId}:{jti},value 是 token,TTL 设为 refresh 过期时间) - 返回给前端
{ accessToken, refreshToken, expiresIn, tokenType: "Bearer" }
这里我特意把 refresh token 存 Redis 有两个目的:一是实现退出登录的吊销,删 key 即可;二是续签时必须确认 refresh token 确实还在有效期内,Redis 本身就是天然的 TTL 管理器,不用自己写定时清理任务。
更新用户登录信息场景也很常见,比如用户改头像、改昵称后,如果 JWT 里存了 username、昵称这些展示型字段,旧令牌里的信息就过期了。两种解决思路:一是 JWT 里只存 userId,需要展示信息时动态查库(我推荐这种);二是要求用户重新登录。第一种对数据库的查询压力不大,单体应用完全扛得住,而且避免了很多令牌同步更新的麻烦。
4.3 令牌续签方案与安全细节
令牌续签是 JWT 方案里最容易翻车的地方,我见过太多人直接不做续签,导致用户刷着刷着突然被踢下线。我的方案是滑动续签:每次请求携带 access token 时,如果剩余有效期低于预设阈值(比如还差 5 分钟过期),就通过 refresh token 换取新的 access token 返回给前端,前端在响应拦截器里判断到新 token 就自动替换。
实现上要注意并发场景的幂等性:连续两个请求同时触发续签,可能会生成两个新 access token。我用的办法是给 refresh token 加一个version字段,续签前先做 CAS( compare-and-swap )更新版本号,版本号变了就说明已有续签发生,直接复用现有结果,不重复签发。
JWT 还有一个经典安全漏洞是算法混淆攻击,攻击者把 header 里的alg改成none或者把 HS256 改成 RS256 来伪造签名。这就是热搜里jwt kid相关漏洞的根源——kid参数用来指定密钥 ID,如果没有在服务端做好密钥绑定校验,攻击者就能通过伪造kid诱导服务端用错误的密钥验签。规避方法:验签时强制校验alg必须是白名单算法,且kid必须对应到服务端已知的密钥 ID。
4.4 验证码集成与登录安全
很多登录场景除了账号密码还要验证码,我在脚手架里提供了验证码的集成位。思路很简单:生成随机四位数字或字母验证码,存储到 Redis,key 为captcha:{uuid},TTL 设 5 分钟;登录时前端携带 uuid 和用户输入的验证码,后端取出 Redis 的值比对,比对成功删掉这个 key(防止重复使用),再进行密码校验。
这里有个值得注意的细节:验证码必须在密码校验之前验证。如果先验密码再验验证码,攻击者可以用正确密码反复撞验证码,验证码成了摆设。反过来先验验证码,即使验证码被爆破,攻击者还要面对密码校验,防御纵深更合理。验证码图片本身我用的 Hutool 的CaptchaUtil,一行代码生成,开发环境足够用,生产环境可以考虑换成更防 OCR 的滑块类验证码。
5. Docker 容器化部署
5.1 多阶段构建 Dockerfile 实战
Spring Boot 3.x 官方推荐的方式是用多阶段构建,先在一个容器里编译打包,再在另一个精简容器里只放运行产物。这样镜像里不残留编译工具链,体积能小很多。我的 Dockerfile 长这样:
# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 运行阶段 FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=builder /build/target/demo-server.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]这里dependency:go-offline是为了把依赖先拉下来缓存,这样源码改动后重新构建不用再重新下依赖,构建速度快很多。运行阶段的-XX:+UseContainerSupport -XX:MaxRAMPercentage=75是 JDK 对容器场景的适配参数,让 JVM 能自动识别容器内存限制,而不是按宿主机内存配置来分配堆大小。
构建命令:
docker build -t demo-server:1.0.0 . docker run -d --name demo-server -p 8080:8080 demo-server:1.0.05.2 docker-compose 编排全套依赖
单体应用虽然只有一个应用容器,但依赖的中间件不少:MySQL、Redis、Nacos,一个一个docker run太累了。我推荐用 docker-compose 统一编排:
version: '3.8' services: nacos: image: nacos/nacos-server:v2.5.4 container_name: nacos environment: - MODE=standalone - NACOS_AUTH_ENABLE=true ports: - "8848:8848" - "9848:9848" networks: [app-net] mysql: image: mysql:8.0 container_name: mysql environment: - MYSQL_ROOT_PASSWORD=root123 - MYSQL_DATABASE=demo volumes: - mysql-data:/var/lib/mysql ports: - "3306:3306" networks: [app-net] redis: image: redis:7 container_name: redis ports: - "6379:6379" volumes: - redis-data:/data networks: [app-net] app: image: demo-server:1.0.0 container_name: demo-server depends_on: - nacos - mysql - redis environment: - SPRING_CLOUD_NACOS_SERVER_ADDR=nacos:8848 - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/demo ports: - "8080:8080" networks: [app-net] volumes: mysql-data: redis-data: networks: app-net:depends_on只能保证启动顺序,不能保证依赖就绪。Nacos 容器起来了不代表 8848 端口就能服务,Spring Boot 应用启动时可能还是会连不上 Nacos 报错。我一般会在应用镜像的启动命令里加一段等待逻辑,或者用 Spring Cloud 的失败重试机制配置spring.cloud.nacos.discovery.fail-fast=false,让注册失败不阻断启动。
5.3 容器健康检查与资源限制
生产环境部署容器一定不能裸奔,健康检查和资源限制是必须配的。docker-compose 里给应用加上:
app: healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s deploy: resources: limits: memory: 2g cpus: "1.5" reservations: memory: 1g如果基础镜像里没有 curl,健康检查也可以用 Java 的java -version这种不涉及网络的方式,但监控不了服务的真实健康状态。所以我建议在 Spring Boot 里引入spring-boot-starter-actuator,把/actuator/health作为健康检查端点。actuator 默认暴露端点有限,需要在配置里放开:
management: endpoints: web: exposure: include: health,info资源限制一定要加,否则某个服务内存溢出会拖垮整台机器,Docker 的-m限制配合 JVM 容器感知参数,才能保证应用在限定资源内运行而不是和宿主抢内存。Windows 上如果遇到Docker Desktop failed to start because virtualization support is not detected的报错,多半是 BIOS 里的 Hyper-V 或者 Windows 虚拟化没开,需要在 BIOS 设置里启用虚拟化技术,这也是新手部署时最常见的卡点。
6. 常见问题与排查技巧实录
6.1 问题速查清单
把这些年实际部署运维中见过的高频问题整理成一张速查表,遇到问题先照着排查:
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 启动时连接 Nacos 超时 | 9848 gRPC 端口未暴露 | 检查防火墙和容器端口映射 |
| 服务能注册但配置拉不下来 | dataId 命名不规范 | 按${app.name}-${profile}.yaml建配置 |
Nacos 控制台提示request error, please try again later | 鉴权密钥未配置或 token 过期 | 确认NACOS_AUTH_TOKEN长度大于 32 字节 |
| Nacos 页面修改密码报错 | 管理员密码错误或集群模式不支持 | 单机版在控制台重置密码或改库 |
JWT 解析报WeakKeyException | 密钥长度不足 256bit | 改用 Base64 编码的长随机密钥 |
| JWT 算法混淆 / kid 伪造 | 未校验 alg 白名单 | 验签前强制指定算法,校验 kid 与密钥映射 |
| 容器启动后又立刻退出 | JVM 内存配置超容器限制 | 用MaxRAMPercentage代替固定Xmx |
| Docker Desktop 启动失败 | 虚拟化未开启或 Hyper-V 未启用 | BIOS 开启 VT-x/AMD-V,检查 Windows 功能 |
| JDK17 应用启动报反射访问警告 | 框架依赖模块化未适配 | 升级依赖版本,必要时加--add-opens |
| 配置更新后 Bean 没热刷新 | 配置类缺@RefreshScope | 给@ConfigurationProperties类加注解 |
| 登录接口被暴力撞库 | 验证码校验顺序不对 | 先验验证码再验密码,Redis 验证码一次性删除 |
6.2 关于 Nacos 鉴权与未授权访问的补充
前面反复提到 Nacos 的鉴权一定要开,这里专门展开说说。Nacos 1.x 时代的默认配置是不开启安全认证的,控制台和数据接口全部裸奔,任何人只要能访问到 8848 端口,就能拉取所有服务注册信息和配置内容,包括数据库密码、第三方密钥这些敏感配置。这就是热搜里提到的nacos namespaces 未授权访问漏洞的本质原因。
开启鉴权后,服务端会校验每个请求的 token,客户端接入时也必须通过username/password登录换取 accessToken,这个 token 会作为请求参数附加在请求上。Nacos 2.x 的客户端如果配置了用户名密码但服务器开了 token,启动时会出现403或失败提示,原因基本就是用户密码没对上或者 token 过期。
另外 Nacos 控制台的登录密码修改有自己的一套逻辑,很多人直接在数据库里改users表导致控制台报request error之类的错误,因为密码字段是用 BCrypt 加密的,不能明文硬改。正确做法是在控制台的用户管理页面修改,或者用官方提供的密码重置脚本。
6.3 JDK17 与中间件兼容性的坑
从 JDK8 升到 JDK17 后,最大的兼容性问题不在业务代码里,而在字节码操作和反射领域。Nacos 客户端早期版本使用ReflectionUtils设置私有字段时,会触发 JDK17 强封装限制,日志里会刷出大量IllegalAccessException警告,虽然不影响功能但看着很闹心。
解决方案有两个层次:优先升级 Nacos 客户端到 2.2+ 版本,官方已经适配了 JDK17 的模块化限制;如果公司依赖的旧库实在没法升级,还可以在 JVM 启动参数里加--add-opens java.base/java.lang=ALL-UNNAMED这类白名单放行,但这是临时方案,生产环境不建议长期用。
还有一个隐蔽问题:Lombok 版本太旧在 JDK17 下编译出来的getter/setter会有问题。所以脚手架里 Lombok 一定用 1.18.30 以上的版本,否则 IDEA 编译时会报java: 程序包lombok不存在或者直接编译失败。
6.4 实际踩坑记录与复盘
最后分享几个我做这套脚手架时真实踩过的坑,每个都花了不少时间排查,写出来希望大家绕开走。
第一个是 Nacos 2.x 的 gRPC 端口问题。我第一次部署时只映射了 8848,结果应用启动反反复复报connect timed out,看日志以为是网络问题,折腾了半个多小时才发现是 9848 端口没暴露。Nacos 2.x 的服务注册和配置拉取默认走 gRPC,客户端会连 8848+1000 也就是 9848,容器部署时只开 8848 等于把服务端的主干道关了。排查方法是抓包看实际连接目标端口,或者看 Nacos 服务端日志里客户端的连接来源端口。
第二个是动态刷新的坑。我的DataSourceProperties类加了@ConfigurationProperties但没加@RefreshScope,结果在 Nacos 上改了数据库连接地址,日志里显示配置已变更,但数据源还是连的旧库。后来发现@ConfigurationProperties类默认不会自动重新绑定,必须加上@RefreshScope才算完整。这个坑在网上被问烂了,属于典型的配置生效但 Bean 不刷新的问题。
第三个是 Docker 镜像构建时依赖下载太慢。一开始我把mvn clean package直接跑在构建阶段,每次源码变动都触发全量依赖重新拉取。后来改成先dependency:go-offline把依赖层缓存,再拷贝源码编译,构建时间从十分钟级别降到了一分钟级别。这个优化在生产流水线上尤其宝贵,建议大家都做一下。
第四个是关于 JWT 密钥的初始化方式。我最初在application.yml里写了个明文短密钥demo-secret,结果 jjwt 直接抛WeakKeyException,因为 HS256 至少要 32 字节的密钥。改成 64 字节的 Base64 随机密钥后问题解决。生产环境这个密钥还要通过 Nacos 配置管理,不要写死在代码里,同时要定期轮换。
7. 后续扩展与个人心得
脚手架搭完之后,有几个方向可以继续扩展。一个是把服务拆分出来,现在已经有了 Nacos 注册中心和配置中心的基础,拆出用户服务、订单服务时,只需要在新增服务里引入同样的 Nacos 依赖,注册发现和配置管理都是自动接管的,不用重新设计。另一个是引入网关层,Spring Cloud Gateway 加进来后,JWT 的校验可以下沉到网关统一做,后端各服务只需要信任网关传递过来的用户信息即可,认证逻辑从重复代码里解放出来。
还有一个值得投入的点是 CI/CD。Dockerfile 和 docker-compose 文件都已经就绪,接入 GitLab CI 或者 GitHub Actions 就是加个流水线配置的问题:代码提交 → 单元测试 → 构建镜像 → 推送到镜像仓库 → 服务器拉取镜像重启容器。这一步做好之后,发版效率会有质的提升。
就我个人经验而言,搭脚手架最重要的是克制,别为了炫技堆砌不必要的组件。我见过有人起步就上了 Seata 分布式事务、Kafka 消息队列、XXL-Job 定时调度,项目还没跑起来,光中间件就装了七八个,出了问题排查难度呈指数级上升。单体应用的黄金法则是:业务复杂度没到那个份上,就不引入对应的技术复杂度。等业务确实需要了,在脚手架预留的扩展位上加进去,比一开始就全上要稳妥得多。
还有一点小心得:骨架代码里的注释习惯。新项目搭建期写的注释,往往就是未来一年里团队成员理解业务逻辑的唯一参考。我习惯在关键配置、工具类的类头、接口的边界条件处留下简短注释,说明"为什么这么做",而不是"做了什么"。半年后再回来看这套脚手架,你会发现这些注释比代码本身更有价值。
这套脚手架我一直在内部团队维护使用,每次重构都会顺手完善一下细节。如果你照着搭完遇到了任何问题,大概率绕不开的就是 Nacos 端口、动态刷新注解、JWT 密钥长度这三个核心点。把这几个关键位置处理好了,整套体系基本就能稳定跑起来。后续的扩展能力也已经铺好,希望这套从零搭建的脚手架能帮你少走几个弯路,把时间花在业务实现而不是环境搭建上。