☰
Spring Boot 3.x + JDK17+Nacos+JWT+Docker 生产级单体脚手架搭建指南
2026/10/2 9:21:42 网站建设 项目流程

Spring Boot 3.x 出来之后,身边不少朋友还在 JDK8 + Spring Boot 2.x 的组合里观望,问得最多的一句话就是“到底值不值得升级”。我的结论很直接:如果你是新项目,直接上 Spring Boot 3.x + JDK17,别犹豫。JDK17 是 LTS 版本,Spring Boot 3.x 官方最低要求就是它,生态已经成熟。这套组合配合 Nacos 做注册和配置、JWT 做无状态认证、Docker 做交付,能在一个单体项目里把所有生产级要素都补齐。这套脚手架我前前后后踩了大半个月的坑,今天把完整思路和能直接抄走的配置都摊开讲清楚。

适合谁看?想从 2.x 迁移的老手、刚工作一两年的后端开发、以及准备给团队搭基础工程的负责人。内容从 JDK17 装环境开始,一直到 Docker 部署,全程可以跟着复现。

1. 技术选型:为什么锁定这个组合

1.1 为什么是 Spring Boot 3.x + JDK17,不是 JDK8 也不是 JDK21

先说最容易被忽略的一点:Spring Boot 3.x 用了Jakarta EE 命名空间,原本的javax.*包全部改成了jakarta.*。这不仅仅是改个 import 的事,很多老第三方库如果不升级,直接编译不过。所以从 2.x 升 3.x 不是换版本号那么简单,得把依赖体系整个过一遍。

JDK17 的选择理由很实在:它是 LTS 版本,免费商用,官方支持到 2029 年以后。对比 JDK8,17 带来的实际收益不只是语法糖,ZGC 在大堆内存场景下的停顿改善很明显,record、switch表达式、文本块这些特性让代码简洁不少。有人会问为什么不用 JDK21,因为 Spring Boot 3.x 在 17 上跑得已经很稳,21 的虚拟线程虽然诱人,但对单体项目来说还不是刚需,没必要在基础工程里引入额外变量。

还有一点要注意,Spring Boot 3.x 对第三方依赖的最低版本有硬性要求,比如 MyBatis 得 3.5.5+、MySQL 驱动得 8.0.33+。如果你项目里还躺着老版本的 druid 或者小众 ORM,升级前最好先查一遍兼容性清单,别等编译爆红了再手忙脚乱。

1.2 Nacos、JWT、Docker 在单体项目里分别解决什么问题

这套组合里,Nacos 同时承担注册中心和配置中心两个角色。有人会觉得单体架构用不上注册中心,其实不然。单体项目一旦部署多个实例做负载均衡,实例上下线就需要被网关或负载设备感知到;而配置中心的价值更大,数据库连接池参数、开关类配置、接口阈值这些都能动态调整,不用重启应用。替代品里 Consul、Apollo 都够好,但 Nacos 对 Spring Cloud Alibaba 生态的支持、控制台的易用性、中文文档的完善度,综合起来是最适合 Java 团队的。

JWT 解决的是无状态认证。传统 Session 方案在单体里也没问题,但要考虑分布式扩展时 Session 同步的麻烦。JWT 令牌自带用户标识和过期时间,服务端不需要存会话,天然适合前后端分离和后续拆微服务。代价是令牌一旦签发,在过期前很难主动吊销,所以后面我会讲续签和黑名单的折中方案。

Docker 解决的是环境一致性问题。团队里十个开发、五套系统配置,总有人 MySQL 版本不对、Redis 没装好。用 Docker 把 MySQL、Redis、Nacos 这些基础设施统一编排起来,一条命令拉起整套依赖环境,开发机和生产环境的差异被压缩到最小。

这套组合还有一个隐藏收益:沉淀下来的 docker-compose 文件、Nacos 配置模板、JWT 工具类,可以直接复用到下一个项目。我见过太多团队每个项目从零搭环境,配置还搭得五花八门,脚手架的意义就是把这些公共成本一次性付掉。

1.3 为什么坚持做单体而不是一开始就拆微服务

选型阶段必须面对一个问题:既然上了 Nacos,为什么不干脆拆微服务?我的经验是,大部分业务场景单体更合适。微服务带来的服务间通信开销、分布式事务复杂度、链路追踪成本,在小团队和中等体量业务面前都是纯负担。单体先把业务模型跑通,等某个模块确实出现独立扩缩容需求时,再基于清晰的模块边界拆出去,这是成本最低的演进路径。

所以这个脚手架的定位是:代码上单体,基础设施上保留微服务时代的扩展能力。Nacos、JWT 这些选型,让将来拆服务时不用推翻重来。

2. 环境准备:JDK17 与工程初始化

2.1 JDK17 安装与配置

JDK17 的安装本身不复杂,但有几个地方容易踩坑。Windows 用户建议直接下载Oracle JDK17 或 Eclipse Temurin(Adoptium)的.msi安装包,前者安装完自动配好 JAVA_HOME,后者需要手动加环境变量。Linux 服务器上更推荐的是一键安装:

sudo apt install openjdk-17-jdk

装完先验证版本:

java -version

这里有个坑:如果之前装过 JDK8,PATH里可能还残留旧路径,要确认JAVA_HOME指向的是 17 而不是 8。IDEA 社区版里切换 Project SDK 时,也要手动指向 17。

体感上,很多新手在“JDK17 下载与安装”这一步卡住,其实是没搞清楚渠道。带 Oracle 账号才能下载的版本、清华镜像的 OpenJDK、sdkman 安装的版本,三者都能用,优先选 Temurin 最省心。

2.2 用 IDEA 初始化 Spring Boot 3.x 工程

IDEA 社区版没有 Spring Initializr 的集成向导,但有两个替代路径,都很顺畅。

路径一:去 start.spring.io 生成基础工程再导入。页面里选择Java 17、Spring Boot 3.x,依赖勾选 Spring Web、Validation、Spring Data Redis、MySQL Driver、Lombok。生成后解压,在 IDEA 里直接 Open 这个 Maven 项目。

路径二:手动建一个空 Maven 工程,pom.xml里用 parent 继承 Spring Boot 父工程:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent>

然后添加所需 starter。我推荐路径二,因为它让你清楚每个依赖是怎么进来的,而不是无脑勾选。

工程创建后的第一件事,是确认 Maven 的 JDK 编译级别。在pom.xml显式声明:

<properties> <java.version>17</java.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties>

不显式声明的话,Maven 默认可能用 JDK8 编译,导致各种诡异报错。

2.3 依赖版本统一管理的实操做法

Spring Boot 的 parent POM 已经管理了大多数核心依赖版本,但第三方组件需要自己锁版本,尤其是这几个:Nacos 客户端、JJWT 库、MySQL 驱动。忽略版本管理的话,团队里 A 用了 2.2.0 的 JJWT、B 用了 2.3.0,接口签名还不一样,联调时到处是雷。

我的习惯是单独抽一个spring-boot-dependencies-custom的 BOM 模块,或者直接在父pom.xml里用dependencyManagement统一声明:

<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2023.0.1.0</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> </dependencies> </dependencyManagement>

版本统一之后,项目里加依赖只需要写 groupId 和 artifactId,不用反复查版本号,升级时也只改一处。

3. Nacos 落地:注册中心与配置中心一次搞定

3.1 用 Docker 跑 Nacos 并开启鉴权

Nacos 的部署方式选择很多,开发机推荐 Docker 单机版,生产环境建议至少主备。单机启动最简单的方式:

docker run -d --name nacos \ -p 8848:8848 \ -p 9848:9848 \ -e MODE=standalone \ -e NACOS_AUTH_ENABLE=true \ nacos/nacos-server:v2.3.2

这里加粗说明两个端口:8848 是 HTTP 端口,9848 是 gRPC 端口,Spring Cloud Alibaba 从 2021 版本开始走 gRPC 通信,遗漏 9848 端口会导致服务注册后心跳报错,这是最容易踩的坑。

环境变量里NACOS_AUTH_ENABLE=true就是开启鉴权。Nacos 控制台默认账号密码是nacos/nacos,开启鉴权后第一次登录一定要改密码,否则相当于裸奔。更稳妥的做法是在启动参数里预置密钥:

-e NACOS_AUTH_TOKEN=此处填一个自定义的Base64编码密钥

网上有不少关于 Nacos 未授权访问漏洞的讨论,核心原因就是没开鉴权或者密钥用默认值,如果你负责生产环境的 Nacos,务必检查这两项。

3.2 服务注册与数据源连接配置

应用接入 Nacos 做注册中心,依赖配一下:

<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>

application.yml里的关键配置:

spring: application: name: demo-server cloud: nacos: server-addr: 127.0.0.1:8848 username: nacos password: 修改后的密码 discovery: namespace: dev group: DEFAULT_GROUP config: namespace: dev file-extension: yaml refresh-enabled: true

注意file-extension必须设成 yaml,因为 Nacos 控制台新建配置时你可以选 Data ID 格式,但客户端这边要明确告知解析格式,否则默认按 properties 解析,配置内容识别不了。

如果项目里用了达梦这类国产数据库,Nacos 客户端和驱动也要适配。Nacos 2.5.4 连接达梦数据库时,需要在 Nacos 的配置文件里把数据源驱动换成达梦的 JDBC 驱动类,并在application.properties里指向达梦的连接 URL。这块在官方文档里写得比较简略,实操时要是发现 Nacos 后台能打开但配置持久化报错,第一反应就查数据库驱动和方言配置。

3.3 配置分组、命名空间与多环境管理

Nacos 配置管理的三个层次容易搞混:命名空间(Namespace)、分组(Group)、Data ID。我拿文件夹类比一下:

  • 命名空间:相当于整个环境隔离,dev、test、prod 各一套配置文件,互不干扰。
  • 分组:相当于项目分类,同一个环境里多个项目,用分组区分。
  • Data ID:相当于文件名,Spring Boot 应用默认找{spring.application.name}.{file-extension}这个 Data ID。

实际项目里配置分层的标准做法是:公共配置放一个 Data ID,各环境差异配置放不同命名空间。比如:

demo-server.yaml # 公共配置:端口、应用名、日志级别 demo-server-datasource.yaml # 数据源配置 demo-server-redis.yaml # 缓存配置

然后通过spring.cloud.nacos.config.extension-configs引入多个 Data ID:

spring: cloud: nacos: config: extension-configs: ->@RefreshScope @Component @ConfigurationProperties(prefix = "order") public class OrderConfig { private Integer timeoutSeconds; private Boolean autoCancel; // getter / setter }

在 Nacos 控制台修改配置后,几秒内应用就能感知。但有三个坑必须提醒:

第一,@Value注入的字段必须配合@RefreshScope才能刷新,如果是static字段,刷新会失效,因为静态字段不属于实例作用域。

第二,数据源连接池参数刷新不一定能完全热生效。比如最大连接数,连接池初始化后内部可能不会动态调整,最保险的做法是刷新回调里重建连接池,或者接受这类参数需要重启。

第三,动态刷新日志默认级别是 INFO,遇到“明明改了没生效”的疑难问题,先把com.alibaba.nacos的日志级别调到 DEBUG,看客户端是否真的收到了推送,大概率是网络或配置格式问题。

4. JWT 认证:从登录到鉴权的完整实现

4.1 JWT 令牌从登录到校验的完整链路

JWT 认证流程画出来其实很简单,但每个环节都有实现细节。链路是:

  1. 用户提交账号密码(外加验证码),登录接口校验通过后生成 JWT 令牌。
  2. 前端拿到令牌后,每次请求在Authorization请求头带上Bearer <token>。
  3. 后端拦截器或过滤器解析令牌,校验签名和过期时间,从令牌里取出用户信息放入上下文。
  4. 接口层从上下文拿当前用户,做权限判断。

JWT 的结构分三部分:Header(签名算法和类型)、Payload(用户信息、签发时间、过期时间)、Signature(签名),三部分用点号连接。签名的作用是防止令牌内容被篡改,服务端只保留密钥,不用存储会话。

核心工具类我习惯用 JJWT 0.11.5 实现。生成令牌:

private String createToken(String userId, String username) { long now = System.currentTimeMillis(); return Jwts.builder() .setSubject(userId) .claim("username", username) .setIssuedAt(new Date(now)) .setExpiration(new Date(now + EXPIRE_MILLIS)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); }

解析令牌:

public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); }

这里有个重要细节:HS256 的密钥长度必须大于等于 256 位(32 字节),否则 JJWT 会抛WeakKeyException或签名异常。很多新手用短字符串当密钥,总是报错,查了半天才发现是长度不够。

4.2 集成 Spring Security 还是拦截器

这是单体项目里讨论最多的问题之一。我的建议分情况:如果是纯后端 API 且认证模式单一,用拦截器完全够;如果涉及复杂权限模型、方法级权限、OAuth2 集成,直接上 Spring Security。

我用 Spring Security 的做法是:自定义一个OncePerRequestFilter,专门解析和校验 JWT,把认证信息放进SecurityContextHolder:

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = resolveToken(request); if (token != null && jwtService.validateToken(token)) { String userId = jwtService.getUserId(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); SecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(request, response); } }

然后 SecurityConfig 里把这些接口列为白名单,其他路径统一需要认证:

http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.stateless()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/login", "/api/auth/captcha").permitAll() .anyRequest().authenticated()) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

注意csrf必须关闭,因为 JWT 模式是无状态 API,CSRF 令牌机制反而会干扰。跨域配置要单独处理,在 CorsFilter 里放行带Authorization头的请求。

4.3 验证码、Token 续签与安全加固

登录验证码是 SPA 项目里常见的配置。实现方式:后端生成一个随机的 4 位或 6 位验证码,用 Redis 存起来(key 为 uuid,value 为验证码),同时把验证码图片的 Base64 返回给前端。用户提交登录时,带上 uuid 和输入值,后端对比后再走账号密码校验。

Redis 存储验证码的过期时间建议 5 分钟,校验通过后立即删除。不管是网上给的 5 分钟还是更短,验证码必须有失效时间和单次有效限制,防止暴力重放。

Token 续签是 JWT 体系里的经典难题。JWT 令牌本身无状态,过期后你没法自己续,最主流的方案是双令牌机制:Access Token 有效期短(30 分钟),Refresh Token 有效期长(7 天)。Access Token 过期后,前端用 Refresh Token 调一个刷新接口,换取新的 Access Token。

实现时有两个细节:Refresh Token 接口要校验 Refresh Token 的签名、过期时间和是否在黑名单里;更换令牌时要让旧 Access Token 立即失效,做法是把令牌放进 Redis 黑名单,TTL 设为剩余有效期。这样既享受了 JWT 无状态的优点,又补上了“主动吊销”的短板。

安全加固方面,结合网上讨论的 JWT 漏洞总结,至少要做这几件事:

  • 密钥复杂度要高,不能是硬编码的短字符串,建议配置中心下发,定期轮转。
  • Payload 里不要放敏感信息,JWT 是 Base64 编码,不是加密,谁都能解码看内容。
  • kid(Key ID)参数如果用了,必须校验密钥来源,防止攻击者通过篡改kid注入恶意密钥。
  • 设置合理的过期时间,并配合 Refresh Token 机制,尽量缩小令牌泄露的暴露窗口。

5. Docker 化部署:让环境和应用一起交付

5.1 编写 Dockerfile 构建应用镜像

JDK17 应用打镜像,最推荐的思路是多阶段构建。第一阶段用 Maven 容器编译,第二阶段用精简 JRE 运行,这样最终镜像体积能缩小一大半。

# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests -B # 运行阶段 FROM eclipse-temurin:17-jre-jammy WORKDIR /app COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

写 Dockerfile 时有三个容易被忽略的点:

第一,mvn dependency:go-offline这一步很关键,它先把依赖下载缓存到镜像层,之后每次改代码重新构建时,只要pom.xml没变就走缓存,构建速度快很多。

第二,Java 应用跑在容器里要注意内存参数。JVM 默认可能申请宿主机四分之一的内存,这在开发机没问题,但生产环境很容易把容器内存撑爆。建议显式设置堆内存:

java -Xms256m -Xmx512m -jar app.jar

第三,时区问题。容器默认是 UTC 时区,Java 应用打日志时间和本地差 8 小时,镜像里补一句:

ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

5.2 docker-compose 编排 MySQL、Redis 与 Nacos

基础设施用 docker-compose 编排,一条命令拉起全套依赖。这是整个脚手架里性价比最高的部分,我直接给一个能用的模板:

version: "3.8" services: mysql: image: mysql:8.0 container_name: scaffold-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: scaffold TZ: Asia/Shanghai ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7.0 container_name: scaffold-redis command: redis-server --requirepass redis123456 ports: - "6379:6379" volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 nacos: image: nacos/nacos-server:v2.3.2 container_name: scaffold-nacos environment: MODE: standalone NACOS_AUTH_ENABLE: "true" NACOS_AUTH_TOKEN: "xxxxxxxx" SPRING_DATASOURCE_PLATFORM: "mysql" MYSQL_SERVICE_HOST: mysql MYSQL_SERVICE_DB_NAME: nacos_config MYSQL_SERVICE_USER: root MYSQL_SERVICE_PASSWORD: root123456 ports: - "8848:8848" - "9848:9848" depends_on: mysql: condition: service_healthy app: build: . container_name: scaffold-app depends_on: mysql: condition: service_healthy nacos: condition: service_started ports: - "8080:8080" environment: SPRING_PROFILES_ACTIVE: dev TZ: Asia/Shanghai volumes: mysql-data: redis-data:

注意depends_on加condition: service_healthy是一种优雅的启动顺序控制方式。直接depends_on只是启动顺序,容器启动不代表服务就绪,MySQL 还没初始化完成应用就连库,必定失败。加了健康检查之后,等 MySQL 真正 ready 再启动应用,这个坑能省下大量排查时间。

Redis 主从配置如果也要纳入编排,建议用redis:7.0起两个服务,master 端口 6379,slave 通过--replicaof master 6379指定主库,然后用redis-sentinel或者业务侧读写分离去适配。

5.3 Docker Desktop 在 Windows 上的安装与启动

Windows 用户装 Docker Desktop,最常遇到的报错是Virtualization support not detected或者Docker Desktop failed to start。这类问题 90% 是 Windows 虚拟化相关功能没开。

排查顺序应该是这样:

  1. 打开“任务管理器 → 性能”,确认虚拟化已启用。
  2. 如果没启用,去 BIOS/UEFI 里打开 Intel VT-x 或 AMD-V。
  3. 打开“控制面板 → 程序 → 启用或关闭 Windows 功能”,勾选“适用于 Linux 的 Windows 子系统(WSL2)”和“虚拟机平台”。
  4. 执行wsl --set-default-version 2把默认版本切成 WSL2。
  5. 重启 Docker Desktop。

Windows 家庭版用户还要注意,Hyper-V 功能默认没有,需要先安装 WSL2 再启动 Docker Desktop。这一步处理完之后,之前卡住的 docker 命令基本就能跑起来了。另外一点,Docker Desktop 的资源设置里,内存建议至少分给 4GB,否则同时跑 MySQL、Redis、Nacos 三个容器会频繁 OOM。

6. 生产环境避坑:高频问题与排查实录

6.1 Nacos 疑难杂症排查清单

Nacos 日常使用中我遇到最多的问题,列一个排查表。

现象可能原因处理方式
服务注册成功但调用报“找不到实例”服务名不一致,或 gRPC 端口 9848 未开放先核对应用名,再检查防火墙和端口映射
应用启动报 Nacos 连接超时Nacos 服务未就绪,客户端配置地址错误Nacos 先起后再启动应用,检查 server-addr
改了配置不刷新缺少@RefreshScope,或 Data ID 文件扩展名不对补注解,检查file-extension: yaml
控制台修改密码报request error, please try again later!Nacos 鉴权密钥未配置或 token 缓存异常配置NACOS_AUTH_TOKEN,清浏览器缓存重新登录
Nacos 页面能打开但配置持久化失败使用外部数据库时驱动不匹配或连接失败检查 MySQL 驱动、建表 SQL、数据库地址

这里特别说下配置中心动态刷新不生效的问题。之前帮人排查过一次,后台配置改了半天,应用日志里完全没有刷新的痕迹。最后发现是 Spring Boot 3.x 里bootstrap上下文默认关闭,导致 Nacos 配置在应用启动初期没加载进来。解决办法是引入依赖并开启 bootstrap:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>

或者在application.yml里用spring.config.import方式导入 Nacos 配置,这个在 Spring Cloud 2021 之后更推荐。另外,Nacos 2.5.4 连接达梦数据库这类场景,除了换驱动,还要确认 Nacos 的配置文件里spring.jpa.database-platform、spring.datasource.driver-class-name都同步改掉,缺失一项就会出现“页面正常但数据写不进库”的怪现象。

6.2 JWT 与 Spring Security 高频问题

JWT + Spring Security 的集成是单体脚手架里出错率最高的部分。常见问题有三个:

第一个是AuthenticationEntryPoint没配置,导致未登录请求返回 403 而不是 401 JSON。Spring Security 默认行为对表单登录友好,但对 API 不友好。需要自定义一个类:

@Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录或凭证已过期\"}"); } }

然后在 SecurityConfig 里配置http.exceptionHandling(e -> e.authenticationEntryPoint(restAuthenticationEntryPoint))。

第二个是Spring Security 放行路径与拦截器冲突。如果同时存在自定义 HandlerInterceptor 和 Spring Security 过滤器,注意/api/auth/login虽然是 Security 白名单,但自定义拦截器可能仍然会执行并尝试解析 token,结果登录接口被自己的拦截器拦了。解决办法是在自定义拦截器里也加上白名单判断,或者干脆统一用 Spring Security 的过滤器链路。

第三个是跨域配置与携带 Authorization 头的兼容问题。前端如果是独立域名,必须配置 CORS 且允许Authorization请求头:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } }

注意allowedOrigins不要用*配allowCredentials(true),浏览器会报错,必须写具体域名。另外 Spring Security 的 CORS 配置和 Spring MVC 的 CORS 配置如果同时存在,容易产生两套 filter 链,建议在 SecurityConfig 的http.cors()里统一管理。

6.3 Docker 容器应用的日志与运维

应用容器化之后,日志处理方式跟以前完全不同。容器里的应用不该把日志写到文件,而是输出到 stdout/stderr,由 Docker 统一收集。Spring Boot 默认日志就输出到控制台,这点刚好符合容器习惯。

排查问题时的标准命令:

# 查看应用实时日志 docker logs -f scaffold-app # 进入容器排查 docker exec -it scaffold-app bash # 查看容器资源占用 docker stats scaffold-app

日志里发现容器频繁重启,最常见的原因是 JVM 堆内存设置超过容器可用内存,导致被 OOM Killed。这时候用docker inspect scaffold-app | grep -i memory看容器限额,同时把 JVM 的-Xmx调低。更规范的做法是在启动命令里用-XX:MaxRAMPercentage=75.0,让 JVM 自动感知容器内存配额,避免硬编码 512m 和容器实际大小不匹配。

还有个日常运维小技巧:docker-compose 里统一约定容器名称与网络别名,这样应用里配置 Nacos 地址时可以直接写nacos:8848,配置 MySQL 地址写mysql:3306,不用在不同的部署环境里改配置。这套约定在开发、测试、生产里都能用,配合 Nacos 配置文件中心的server-addr环境变量,实现了“代码不变、配置随环境走”。

结尾:一些真实的体会

脚手架搭完后再回看这半个多月踩的坑,我心里最大的感受是:真正耗时间的地方往往不在写业务代码,而在基础设施的协作细节。比如 Nacos 开启鉴权之后,服务注册密码不对导致的应用启动失败;比如 Docker Desktop 虚拟化没打开,白白排查了半天;比如 Spring Security 白名单和自定义拦截器的叠加逻辑,光看文档自己是想不到的。

最后分享一个后续可以扩展的方向:这套脚手架里虽然做的是单体,但 Nacos、JWT、Docker 这些基础设施已经为拆分留好了口子。想更进一步,可以把通用能力独立成模块,比如抽出scaffold-common放 JWT 工具类、统一返回结构、异常处理;再往上可以接 Spring Cloud Gateway 做统一入口,把认证从单体应用里剥离出来变成网关层的公共逻辑。但是这些都是后话,先把单体跑稳、把基础设施沉淀好,比什么都重要。

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

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

立即咨询