从零搭建Spring Boot3.x生产级单体脚手架项目(JDK17 + Nacos + JWT + Docker)

🏷️ 前言

在实际的企业级开发中,我们往往面临这样一个场景:业务初期不需要复杂的微服务架构,一个稳健、标准、高扩展的单体应用才是最佳选择。

网上的资源大部分还停留在 Boot 2.x 甚至 JDK 8 的时代,配置也多半是"玩具级"的------只有基础 CURD,像RSA+JWT 鉴权全局异常兜底COSR跨域Redis 双重缓存 以及标准化的 Docker 部署这些生产必备的组件,基本找不到现成整合好的。

既然找不到标准的,就自己动手搭一套。这篇博客就是为了解决这个痛点:搭建一套打磨好的生产级底座,确保你拉下来改个包名,就能直接投入开发,别再浪费时间重复造轮子了。

源码地址https://github.com/RemainderTime/spring-boot-base-demo
(建议 Clone 下来对照阅读,本文基于 master 分支)

⚙️ 环境准备

  • JDK : 17+ (Spring Boot 3.x 硬性要求)
  • Maven: 3.8+
  • MySQL: 8.0+
  • Redis: 5.0+
  • Nacos: 2.x (作为配置中心,可选但推荐)

🚀 实践步骤

1. 工程核心依赖管理

使用 Maven 创建标准 Spring Boot 工程,核心依赖如下 (pom.xml):

XML 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.3</version>
</parent>
<properties>
    <java.version>17</java.version>
    <mybatis-plus.version>3.5.8</mybatis-plus.version>
    <spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
    <springdoc.version>2.6.0</springdoc.version>
</properties>
<dependencies>
    <!-- Web 模块 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Cloud Alibaba Nacos Config -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        <version>${spring-cloud-alibaba.version}</version>
    </dependency>
    <!-- Spring Cloud Alibaba Nacos Discovery -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <version>${spring-cloud-alibaba.version}</version>
    </dependency>
    <!-- MyBatis Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        <version>${mybatis-plus.version}</version>
    </dependency>
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- JWT (鉴权核心) -->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.19.2</version>
    </dependency>
    <!-- SpringDoc (OpenAPI 3 文档) -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>${springdoc.version}</version>
    </dependency>
</dependencies>

2. Nacos 配置变革 (Configuration)

Spring Boot 2.4+ 推荐使用 import 方式导入配置,摒弃 bootstrap.yml

本地引导配置 ( src/main/resources/application.yml)

bash 复制代码
server:
  port: 8089
  shutdown: graceful

spring:
  profiles:
    active: dev
  application:
    name: xf-boot-base
  cloud:
    nacos:
      config:
        server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
        file-extension: yml
        group: DEFAULT_GROUP
        namespace: YOUR_NAMESPACE_ID # 你的 Nacos 命名空间ID
  # 核心改动:通过 import 导入远程配置
  config:
    import:
      - nacos:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

Nacos 远程配置内容 ( xf-boot-base-dev.yml)
直接新建配置,粘贴以下生产级参数。注意我们定义了一个 global属性来存放 RSA 秘钥。

bash 复制代码
spring:
  datasource:
    dynamic:
      primary: master
      hikari: 
        maximum-pool-size: 10
      datasource:
        master:
          url: jdbc:mysql://127.0.0.1:3306/xf-boot-base?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
          username: root
          password: password
          driver-class-name: com.mysql.cj.jdbc.Driver
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      lettuce:
        pool:
          maxActive: 100
          maxIdle: 30

# 自定义全局配置
global:
  # RSA 私钥 (用于解密前端传来的密码)
  rsaPrivateKey: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL8XlRALVBay7dCw...

扩展点与注意事项:

  • Nacos 动态刷新 : 如果需要在不重启服务的情况下动态调整日志级别或业务开关,可以在类上添加 @RefreshScope 注解。本项目的 GlobalConfig 类已经具备此能力(属性注入),修改 Nacos 配置后会自动生效。

3. 核心基础配置 (Infrastructure)

在写业务前,必须先把基础设施搭好:跨域、分页、文档

跨域配置 ( GlobalCorsConfig.java)
前后端分离必踩之坑,这里直接上万能配方。

java 复制代码
@Configuration
public class GlobalCorsConfig {
    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOriginPattern("*"); // 允许所有域
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setAllowCredentials(true); // 允许 Cookie
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(0); // 优先级最高
        return bean;
    }
}

MyBatis Plus 分页配置 ( MybatisPlusConfig.java)

java 复制代码
@Configuration
@MapperScan("cn.xf.basedemo.mappers")
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 添加乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

Swagger/SpringDoc 配置 ( SwaggerGroupApi.java)

java 复制代码
@Configuration
public class SwaggerGroupApi {
    @Bean
    public OpenAPI apiInfo() {
        return new OpenAPI()
                .info(new Info().title("XF-Boot-Base API").version("v1.0"))
                // 配置 JWT 安全认证按钮
                .components(new Components()
                        .addSecuritySchemes("Authorization",
                                new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")))
                .addSecurityItem(new SecurityRequirement().addList("Authorization"));
    }
}

Redis 序列化配置 ( RedisConfig.java)

java 复制代码
@Configuration
public class RedisConfig {
    @Resource
    private RedisConnectionFactory factory;

    @Bean
    public Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer(){
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);
        return serializer;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer()); // 关键点:使用 JSON 序列化
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

扩展点与注意事项:

  • Redis 序列化 : 生产环境中强烈建议使用 GenericJackson2JsonRedisSerializerJackson2JsonRedisSerializer 替代默认的 JDK 序列化,这样在 Redis Desktop Manager 中看到的 VALUE 才是可读的 JSON 字符串,方便调试与排查问题。

4. 全局异常处理 (Global Exception)

这是区分"Demo"和"产品"的关键。通过 @RestControllerAdvice 统一接管异常。

全局异常处理器 ( GlobalExceptionHandler.java)

java 复制代码
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 1. 处理自定义登录异常
    @ExceptionHandler(LoginException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public GenericResponse<Void> handleLoginException(LoginException e) {
        log.warn("认证失败: {}", e.getMessage());
        return new GenericResponse<>(403, null, e.getMessage());
    }

    // 2. 处理参数校验异常 (@Validated 触发)
    @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public GenericResponse<Void> handleValidationException(Exception e, HttpServletRequest request) {
        String errorMsg = "参数校验失败";
        if (e instanceof MethodArgumentNotValidException) {
             errorMsg = ((MethodArgumentNotValidException) e).getBindingResult().getFieldError().getDefaultMessage();
        }
        return new GenericResponse<>(400, null, errorMsg);
    }

    // 3. 兜底系统异常
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public GenericResponse<Void> handleSystemException(Exception e, HttpServletRequest request) {
        log.error("系统未知异常 [URL:{}]", request.getRequestURI(), e);
        return new GenericResponse<>(500, null, "系统繁忙,请稍后再试");
    }
}

5. 核心业务:安全登录闭环 (Authentication)

常规明文传输密码已无法通过安全审计。我们实现了 RSA前端加密 -> 后端解密 -> 验证 -> 生成JWT -> Redis缓存 的完整链路。

Service 层 ( UserServiceImpl.java)

java 复制代码
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private GlobalConfig globalConfig; // 注入 Nacos 中的 RSA 私钥

    @Override
    public RetObj login(LoginInfoRes res) {
        try {
            // 1. RSA 解密前端传来的密文
            String loginJson = RSAUtils.privateDecryption(res.getEncryptedData(), 
                RSAUtils.getPrivateKey(globalConfig.getRsaPrivateKey()));
            LoginInfo loginInfo = objectMapper.readValue(loginJson, LoginInfo.class);

            // 2. 数据库查验账号密码
            User user = userMapper.selectOne(new QueryWrapper<User>()
                .eq("account", loginInfo.getAccount())
                .eq("password", loginInfo.getPwd()));
            
            if (user == null) return RetObj.error("账号或密码错误");

            // 3. 生成 Token 并双重缓存
            String token = JwtTokenUtils.createToken(user.getId());
            LoginUser loginUser = convertToLoginUser(user, token);
            
            // 缓存1:Token -> UserInfo (用于鉴权)
            redisTemplate.opsForValue().set("token:" + token, JSONObject.toJSONString(loginUser), 3600, TimeUnit.SECONDS);
            // 缓存2:UserId -> Token (用于踢人下线)
            redisTemplate.opsForValue().set("user_login_token:" + user.getId(), token, 3600, TimeUnit.SECONDS);

            return RetObj.success(loginUser);
        } catch (Exception e) {
            log.error("登录异常", e);
            return RetObj.error("登录失败");
        }
    }
}

扩展点与注意事项:

  • 关于 RSA 加密 : 我们使用 RSAUtils.privateDecryption 进行密码解密。请务必去 RSAUtils 工具类中生成你自己的一对公私钥,并将私钥 配置到 Nacos 的 global.rsaPrivateKey 中,公钥提供给前端用于加密密码。

网关级拦截器 ( TokenInterceptor.java)
拦截所有请求,解析 Token 并注入 ThreadLocal

java 复制代码
@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // ... (略去白名单校验)

        // 1. 提取 Token
        String token = request.getHeader("Authorization");
        if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
            token = token.substring(7);
        } else {
            throw new LoginException("未登录");
        }

        // 2. Redis 校验
        String userInfoJson = (String) redisTemplate.opsForValue().get("token:" + token);
        if (StringUtils.isEmpty(userInfoJson)) {
            throw new LoginException("会话已过期");
        }

        // 3. 自动续期
        redisTemplate.expire(token, 86700, TimeUnit.SECONDS);

        // 4. 写入上下文,Controller 直接用
        SessionContext.getInstance().set(JSONObject.parseObject(userInfoJson, LoginUser.class));
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        SessionContext.getInstance().clear(); // 必须清理,防止内存泄漏
    }
}

扩展点与注意事项:

  • ThreadLocal 内存泄漏防范 : 在 afterCompletion 方法中,必须 调用 SessionContext.getInstance().clear()。虽然 ThreadLocal 在线程销毁时会被回收,但在 Web 容器的线程池场景下,线程是复用的,如果不清理,上一个用户的 UserInfo 可能会残留在当前线程中,造成严重的数据泄露 BUG。

6. 可视化演示与前端实现 (Visual Demo & Frontend)

为了直观展示效果,我们提供了简单的 Web 页面进行模拟登录测试。

用户表结构 ( xf_user) :
账号:admin,密码:123456

sql 复制代码
CREATE TABLE `xf_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) DEFAULT NULL COMMENT '姓名',
  `account` varchar(32) DEFAULT NULL COMMENT '账号',
  `password` varchar(32) DEFAULT NULL COMMENT '密码',
  `phone` varchar(11) DEFAULT NULL COMMENT '手机号',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 这里的密码实际存储时应为加密后的,演示为了直观使用明文(配合代码逻辑调整)
INSERT INTO `xf_user` VALUES (1001, '又菜又爱玩', 'admin', '123456', '13800138000', NOW());

Web 页面路由控制器 ( WebController.java):

java 复制代码
@Controller
@RequestMapping("web")
public class WebController {
    // 跳转登录页
    @RequestMapping(value = "/login")
    public String login() {
        return "login"; 
    }

    // 统一页面跳转
    @RequestMapping(value = "/{url}", method = RequestMethod.GET)
    public String skipUrl(@PathVariable(name = "url") String url) {
        return url;
    }
}

前端登录实现 ( login.html):

使用 Vue + ElementUI,集成了 JSEncrypt 进行 RSA 加密。

javascript 复制代码
<script>
    new Vue({
        el: '#app',
        data: {
            rsaPublicKey: '...', // 你的 RSA 公钥
            account: '',
            pwd: ''
        },
        methods: {
            login: function () {
                var json = JSON.stringify({account: this.account, pwd: this.pwd});
                // RSA 公钥加密
                var cipher = this.encryptByPublicKey(json);
                
                axios.post("http://localhost:8089/user/login", {
                    encryptedData: cipher,
                }).then(function (response) {
                    if (response.data.code == 200) {
                        // 登录成功跳转,URL携带 Token
                        var param = "?token=" + response.data.data.token + "&name=" + encodeURI(encodeURI(response.data.data.name));
                        window.location.href = "success" + param
                    } else {
                        alert(response.data.message)
                    }
                });
            },
            encryptByPublicKey: function (val) {
                let encryptor = new JSEncrypt()
                encryptor.setPublicKey(this.rsaPublicKey)
                return encryptor.encrypt(val)
            }
        }
    });
</script>

登录演示:

  • 登录页 : 访问 http://localhost:8089/web/login
  • 成功页: 登录成功后跳转,URL 中携带了 Token,页面解析并展示。

7. Docker 容器化部署 (Deployment)

SpringBoot 3.x 强制 JDK 17,因此 Dockerfile 基础镜像必须升级。

标准 Dockerfile ( Dockerfile)

bash 复制代码
# 必须使用 JDK 17 基础镜像
FROM openjdk:17-jdk-alpine
MAINTAINER xiongfeng

WORKDIR /app
COPY target/xf-boot-base-*.jar app.jar
EXPOSE 8089

# 优化 JVM 参数,并支持环境变量注入
ENTRYPOINT ["java", \
            "-Djava.security.egd=file:/dev/urandom", \
            "-Dfile.encoding=UTF-8", \
            "-Duser.timezone=Asia/Shanghai", \
            "-jar", "app.jar"]

构建与启动命令

bash 复制代码
# 1. 本地编译打包 (跳过单元测试)
mvn clean package -DskipTests

# 2. jar包上传服务器后构建镜像 (注意最后的点)
docker build -t xf-boot-base:1.0.1 .

# 3. 启动容器
docker run -d --name xf-boot-base -p 8089:8089 \
  -e NACOS_SERVER_ADDR=192.168.1.100:8848 \
  xf-boot-base:1.0.1

📝 总结

至此,一套代码规范、安全可靠、配置灵活的 Spring Boot 3.3 生产级底座就搭建完成了。

  • 架构:单体应用,但保留了 Nacos 扩展能力。
  • 安全:RSA+JWT+Redis 全套方案。
  • 规范:全局异常、统一响应、Swagger 文档一应俱全。

你可以直接基于此框架开发业务,或者作为公司内部脚手架的雏形。

相关推荐
黯叶2 小时前
基于 Docker+Docker-Compose 的 SpringBoot 项目标准化部署(外置 application-prod.yml 配置方案)
java·spring boot·redis·docker
say_fall2 小时前
泛型编程基石:C++ 模板从入门到熟练
java·开发语言·c++·编辑器·visual studio
代码笔耕2 小时前
写了几年 Java,我发现很多人其实一直在用“高级 C 语言”写代码
java·后端·架构
txinyu的博客2 小时前
结合游戏场景解析UDP可靠性问题
java·开发语言·c++·网络协议·游戏·udp
一路向北North2 小时前
springboot基础(85): validator验证器
java·spring boot·后端
前端不太难2 小时前
Flutter 状态复杂度,如何在架构层提前“刹车”
flutter·架构·状态模式
数说星榆1812 小时前
在线简单画泳道图工具 PC端无水印
大数据·论文阅读·人工智能·架构·流程图·论文笔记
1.14(java)2 小时前
掌握数据库约束:确保数据精准可靠
java·数据库·mysql·数据库约束
Codeking__2 小时前
Redis——value的数据类型与单线程工作模型
java·数据库·redis