启动测试

在项目的右上角,点击 🐞 (手动绿色)图标以调试运行项目。

控制台显示 Tomcat 已在端口 8080 (http) ,表示应用成功启动

打开浏览器,访问 http://localhost:8080/hello-world,页面将显示 hello world,表示接口正常运行。

在这里插入图片描述

连接数据库

为实现应用与 MySQL 的连接与操作,整合 MyBatis-Plus 可简化数据库操作,减少重复的 CRUD 代码,提升开发效率,实现高效、简洁、可维护的持久层开发。

创建数据库

使用 MySQL 可视化工具 (Navicat) 执行下面脚本完成数据库的创建名为 youlai-boot 的数据库,其中包含测试的用户表

    -- ----------------------------
    -- 1. 创建数据库
    -- ----------------------------
    CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;

    -- ----------------------------
    -- 2. 创建表 && 数据初始化
    -- ----------------------------
    use youlai_boot;
	
	-- ----------------------------
    -- Table structure for sys_user
    -- ----------------------------
    DROP TABLE IF EXISTS `sys_user`;
    CREATE TABLE `sys_user` (
        `id` int NOT NULL AUTO_INCREMENT,
        `username` varchar(64) NULL DEFAULT NULL COMMENT '用户名',
        `nickname` varchar(64) NULL DEFAULT NULL COMMENT '昵称',
        `gender` tinyint(1) NULL DEFAULT 1 COMMENT '性别(1-男 2-女 0-保密)',
        `password` varchar(100) NULL DEFAULT NULL COMMENT '密码',
        `dept_id` int NULL DEFAULT NULL COMMENT '部门ID',
        `avatar` varchar(255) NULL DEFAULT '' COMMENT '用户头像',
        `mobile` varchar(20) NULL DEFAULT NULL COMMENT '联系方式',
        `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',
        `email` varchar(128) NULL DEFAULT NULL COMMENT '用户邮箱',
        `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
        `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
        `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
        `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
        `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
        PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;

    -- ----------------------------
    -- Records of sys_user
    -- ----------------------------
    INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668888', 1, '[email protected]', NULL, NULL, NULL, NULL, 0);
    INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668887', 1, '', now(), NULL, now(), NULL, 0);
    INSERT INTO `sys_user` VALUES (3, 'websocket', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668886', 1, '[email protected]', now(), NULL, now(), NULL, 0);

 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

添加依赖

项目 pom.xml 添加 MySQL 驱动和 Mybatis-Plus 依赖:


<dependency>
	<groupId>com.mysqlgroupId>
	<artifactId>mysql-connector-jartifactId>
	<version>9.1.0version>
	<scope>runtimescope>
dependency>


<dependency>
	<groupId>com.alibabagroupId>
	<artifactId>druid-spring-boot-starterartifactId>
	<version>1.2.24version>
dependency>


<dependency>
	<groupId>com.baomidougroupId>
	<artifactId>mybatis-plus-spring-boot3-starterartifactId>
	<version>3.5.9version>
dependency>
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

配置数据源

src/main/resources/application.properties 文件修改为 src/main/resources/application.yml,因为我们更倾向于使用 yml 格式。然后,在 yml 文件中添加以下内容:

server:
  port: 8080
  
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/youlai_boot?useSSL=false&serverTimezone=Asia/Shanghai&&characterEncoding=utf8
    username: root
    password: 123456

mybatis-plus:
  configuration:
    # 驼峰命名映射
    map-underscore-to-camel-case: true
    # 打印 sql 日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto # 主键策略
      logic-delete-field: is_deleted # 全局逻辑删除字段(可选)
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

增删改查接口

安装 MybatisX 插件

在 IDEA 中依次点击 File → Settings(快捷键 Ctrl + Alt + S),打开设置面板,切换到 Plugins 选项卡,搜索 MybatisX 并安装插件。

image-20241129180051551

自动代码生成

在 IDEA 右侧导航栏点击 Database,打开数据库配置面板,选择新增数据源。

输入数据库的 主机地址用户名密码,测试连接成功后点击 OK 保存。
在这里插入图片描述

配置完数据源后,展开数据库中的表,右击 sys_user 表,选择 MybatisX-Generator 打开代码生成面板。

在这里插入图片描述

设置代码生成的目标路径,并选择 Mybatis-Plus 3 + Lombok 代码风格。

在这里插入图片描述

点击 Finish 生成,自动生成相关代码。

MybatisX 生成的代码存在以下问题:

添加增删改查接口

controller 包下创建 UserController.java,编写用户管理接口:

/**
 * 用户控制层
 *
 * @author youlai
 * @since 2024/12/04
 */
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final SysUserService userService;

    /**
     * 获取用户列表
     */
    @GetMapping
    public List<SysUser> listUsers() {
        return userService.list();
    }

    /**
     * 获取用户详情
     */
    @GetMapping("/{id}")
    public SysUser getUserById(@PathVariable Long id) {
        return userService.getById(id);
    }

    /**
     * 新增用户
     */
    @PostMapping
    public String createUser(@RequestBody SysUser user) {
        userService.save(user);
        return "用户创建成功";
    }

    /**
     * 更新用户信息
     */
    @PutMapping("/{id}")
    public String updateUser(@PathVariable Long id, @RequestBody SysUser user) {
        userService.updateById(user);
        return "用户更新成功";
    }

    /**
     * 删除用户
     */
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        userService.removeById(id);
        return "用户删除成功";
    }

}

 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

在这里插入图片描述

接口测试

重新启动应用,在浏览器中访问 http://localhost:8080/users,查看用户数据。

其他增删改接口可以通过后续整合接口文档进行测试。

集成 Knife4j 接口文档

Knife4j 是基于 Swagger2OpenAPI3 的增强解决方案,旨在提供更友好的界面和更多功能扩展,帮助开发者更便捷地调试和测试 API。以下是通过参考 Knife4j 官方文档 Spring Boot 3 整合 Knife4j 实现集成的过程。

添加依赖

pom.xml 文件中引入 Knife4j 的依赖:

<dependency>
    <groupId>com.github.xiaoymingroupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starterartifactId>
    <version>4.5.0version>
dependency>
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

配置接口文档

application.yml 文件中进行配置。注意,packages-to-scan 需要配置为项目的包路径,以确保接口能够被正确扫描,其他配置保持默认即可。

# springdoc-openapi 项目配置
springdoc:
  swagger-ui:
    path: /swagger-ui.html
    tags-sorter: alpha
    operations-sorter: alpha
  api-docs:
    path: /v3/api-docs
  group-configs:
    - group: 'default'
      paths-to-match: '/**'
      packages-to-scan: com.youlai.boot.controller # 需要修改成自己项目的接口包路径
# knife4j的增强配置,不需要增强可以不配
knife4j:
  enable: true
  # 是否为生产环境,true 表示生产环境,接口文档将被禁用
  production: false
  setting:
    language: zh_cn # 设置文档语言为中文
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

添加接口文档配置,在 com.youlai.boot.config 添加 OpenApiConfig 接口文档配置

package com.youlai.boot.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;

/**
 * OpenAPI 接口文档配置
 *
 * @author youlai
 */
@Configuration
@RequiredArgsConstructor
@Slf4j
public class OpenApiConfig {

    private final Environment environment;

    /**
     * 接口信息
     */

    @Bean
    public OpenAPI openApi() {

        String appVersion = environment.getProperty("project.version", "1.0.0");

        return new OpenAPI()
                .info(new Info()
                        .title("系统接口文档")
                        .version(appVersion)
                )
                // 配置全局鉴权参数-Authorize
                .components(new Components()
                        .addSecuritySchemes(HttpHeaders.AUTHORIZATION,
                                new SecurityScheme()
                                        .name(HttpHeaders.AUTHORIZATION)
                                        .type(SecurityScheme.Type.APIKEY)
                                        .in(SecurityScheme.In.HEADER)
                                        .scheme("Bearer")
                                        .bearerFormat("JWT")
                        )
                );
    }


    /**
     * 全局自定义扩展
     * 

* 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。 * 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。 */ @Bean public GlobalOpenApiCustomizer globalOpenApiCustomizer() { return openApi -> { // 全局添加鉴权参数 if (openApi.getPaths() != null) { openApi.getPaths().forEach((s, pathItem) -> { // 登录接口/验证码不需要添加鉴权参数 if ("/api/v1/auth/login".equals(s)) { return; } // 接口添加鉴权参数 pathItem.readOperations() .forEach(operation -> operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) ); }); } }; } } class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

完善接口文档

完善接口描述

在已有的 REST 接口中,使用 OpenAPI 规范注解来描述接口的详细信息,以便通过 Knife4j 生成更加清晰的接口文档。以下是如何为用户的增删改查接口添加文档描述注解的示例:

@Tag(name = "用户接口")
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final SysUserService userService;

    @Operation(summary = "获取用户列表")
    @GetMapping
    public List<SysUser> listUsers() {
        return userService.list();
    }

    @Operation(summary = "获取用户详情")
    @GetMapping("/{id}")
    public SysUser getUserById(
            @Parameter(description = "用户ID") @PathVariable Long id
    ) {
        return userService.getById(id);
    }

    @Operation(summary = "新增用户")
    @PostMapping
    public String createUser(@RequestBody SysUser user) {
        userService.save(user);
        return "新增用户成功";
    }

    @Operation(summary = "修改用户")
    @PutMapping("/{id}")
    public String updateUser(
            @Parameter(description = "用户ID") @PathVariable Long id,
            @RequestBody SysUser user
    ) {
        userService.updateById(user);
        return "修改用户成功";
    }

    @Operation(summary = "删除用户")
    @DeleteMapping("/{id}")
    public String deleteUser(
            @Parameter(description = "用户ID") @PathVariable Long id
    ) {
        userService.removeById(id);
        return "用户删除成功";
    }

}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

完善实体类描述

SysUser 实体类中为每个字段添加 @Schema 注解,用于在接口文档中显示字段的详细说明及示例值:

@Schema(description = "用户对象")
@TableName(value = "sys_user")
@Data
public class SysUser implements Serializable {

    @Schema(description = "用户ID", example = "1")
    @TableId(type = IdType.AUTO)
    private Integer id;

    @Schema(description = "用户名", example = "admin")
    private String username;

    @Schema(description = "昵称", example = "管理员")
    private String nickname;

    @Schema(description = "性别(1-男,2-女,0-保密)", example = "1")
    private Integer gender;

    @Schema(description = "用户头像URL", example = "https://example.com/avatar.png")
    private String avatar;

    @Schema(description = "联系方式", example = "13800000000")
    private String mobile;

    @Schema(description = "用户邮箱", example = "[email protected]")
    private String email;
    
    // ... 
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

使用接口文档

完成以上步骤后,重新启动应用并访问生成的接口文档。

通过左侧的接口列表查看增删改查接口,并点击具体接口查看详细参数说明及示例值:

接着,可以通过接口文档新增用户,接口返回成功后,可以看到数据库表中新增了一条用户数据:

集成 Redis 缓存

Redis 是当前广泛使用的高性能缓存中间件,能够显著提升系统性能,减轻数据库压力,几乎成为现代应用的标配。

添加依赖

pom.xml 文件中添加 Spring Boot Redis 依赖:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

配置 Redis 连接

application.yml 文件中配置 Redis 连接信息:

spring:
  data:
    redis:
      database: 0    # Redis 数据库索引
      host: localhost  # Redis 主机地址
      port: 6379  # Redis 端口
      # 如果Redis 服务未设置密码,需要将password删掉或注释,而不是设置为空字符串
      password: 123456
      timeout: 10s
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

自定义序列化

Spring Boot 默认使用 JdkSerializationRedisSerializer 进行序列化。我们可以通过自定义 RedisTemplate,将其修改为更易读的 StringJSON 序列化方式:

/**
 *  Redis 自动装配配置
 *
 * @author youlai
 * @since 2024/12/5
 */
@Configuration
public class RedisConfig {

    /**
     * 自定义 RedisTemplate
     * 

* 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer * * @param redisConnectionFactory {@link RedisConnectionFactory} * @return {@link RedisTemplate} */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setValueSerializer(RedisSerializer.json()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setHashValueSerializer(RedisSerializer.json()); redisTemplate.afterPropertiesSet(); return redisTemplate; } } class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

单元测试

src/test/java 目录的 com.youlai.boot 包下创建 RedisTests 单元测试类,用于验证数据的存储与读取。

@SpringBootTest
@Slf4j
class RedisTests {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private SysUserService userService;

    @Test
    void testSetAndGet() {
        Long userId = 1L;
        // 1. 从数据库中获取用户信息
        SysUser user = userService.getById(userId);
        log.info("从数据库中获取用户信息: {}", user);

        // 2. 将用户信息缓存到 Redis
        redisTemplate.opsForValue().set("user:" + userId, user);

        // 3. 从 Redis 中获取缓存的用户信息
        SysUser cachedUser = (SysUser) redisTemplate.opsForValue().get("user:" + userId);
        log.info("从 Redis 中获取用户信息: {}", cachedUser);
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

点击测试类方法左侧的 ▶️ 图标运行单元测试。

运行后,稍等片刻,若控制台成功打印从 Redis 获取的用户信息,则表示 Spring Boot 已成功集成 Redis。

完善 Web 框架

统一响应处理


为什么需要统一响应?

默认接口返回的数据结构仅包含业务数据,缺少状态码和提示信息,无法清晰表达操作结果。通过统一封装响应结构,可以提升接口的规范性,便于前后端协同开发和快速定位问题。

下图展示了不规范与规范响应数据的对比:左侧是默认返回的非标准数据,右侧是统一封装后的规范数据。


定义统一业务状态码

com.youlai.boot.common.result 包下创建 ResultCode 枚举,错误码规范参考 阿里开发手册-错误码设计

package com.youlai.boot.common.result;

import java.io.Serializable;
import lombok.Getter;

/**
 * 统一业务状态码枚举
 *
 * @author youlai
 */
@Getter
public enum ResultCode implements Serializable {

    SUCCESS("00000", "操作成功"),
    TOKEN_INVALID("A0230", "Token 无效或已过期"),
    ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
    SYSTEM_ERROR("B0001", "系统错误");

    private final String code;
    private final String message;

    ResultCode(String code, String message) {
        this.code = code;
        this.message = message;
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

创建统一响应结构

定义 Result 类,封装响应码、消息和数据。

package com.youlai.boot.common.result;

import lombok.Data;
import java.io.Serializable;

/**
 * 统一响应结构
 *
 * @author youlai
 **/
@Data
public class Result<T> implements Serializable {
    // 响应码
    private String code;
    // 响应数据
    private T data;
    // 响应信息
    private String msg;

    /**
     * 成功响应
     */
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setMsg(ResultCode.SUCCESS.getMsg());
        result.setData(data);
        return result;
    }

    /**
     * 失败响应
     */
    public static <T> Result<T> failed(ResultCode resultCode) {
        Result<T> result = new Result<>();
        result.setCode(resultCode.getCode());
        result.setMsg(resultCode.getMsg());
        return result;
    }

    /**
     * 失败响应(系统默认错误)
     */
    public static <T> Result<T> failed() {
        Result<T> result = new Result<>();
        result.setCode(ResultCode.SYSTEM_ERROR.getCode());
        result.setMsg(ResultCode.SYSTEM_ERROR.getMsg());
        return result;
    }

}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

封装接口返回结果

调整接口代码,返回统一的响应格式。

@Operation(summary = "获取用户详情")
@GetMapping("/{id}")
public Result<SysUser> getUserById(
    @Parameter(description = "用户ID") @PathVariable Long id
) {
    SysUser user = userService.getById(id);
    return Result.success(user);
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

效果预览

接口返回结构变为标准格式:

通过以上步骤,接口响应数据已完成统一封装,具备良好的规范性和可维护性,有助于前后端协同开发与错误定位。

全局异常处理


为什么需要全局异常处理

如果没有统一的异常处理机制,抛出的业务异常和系统异常会以非标准格式返回,给前端的数据处理和问题排查带来困难。为了规范接口响应数据格式,需要引入全局异常处理。

以下接口模拟了一个业务逻辑中的异常:

@Operation(summary = "获取用户详情")
@GetMapping("/{id}")
public Result<SysUser> getUserById(
    @Parameter(description = "用户ID") @PathVariable Long id
) {
    // 模拟异常
    int i = 1 / 0;

    SysUser user = userService.getById(id);
    return Result.success(user);
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

当发生异常时,默认返回的数据格式如下所示:

这类非标准的响应格式既不直观,也不利于前后端协作。

全局异常处理器

com.youlai.boot.common.exception 包下创建全局异常处理器,用于捕获和处理系统异常。

package com.youlai.boot.common.exception;

import com.youlai.boot.common.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 *
 * @author youlai
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理系统异常
     * 

* 兜底异常处理,处理未被捕获的异常 */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public <T> Result<T> handleNullPointerException(Exception e) { log.error(e.getMessage(), e); return Result.failed("系统异常:" + e.getMessage()); } } class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

验证全局异常处理

再次访问用户接口 localhost:8080/users/1 ,可以看到响应已经包含状态码和提示信息,数据格式变得更加规范:

自定义业务异常

在实际开发中,可能需要对特定的业务异常进行处理。通过自定义异常类 BusinessException,可以实现更灵活的异常处理机制。

package com.youlai.boot.common.exception;

import com.youlai.boot.common.result.ResultCode;
import lombok.Getter;

/**
 * 自定义业务异常
 *
 * @author youlai
 */
@Getter
public class BusinessException extends RuntimeException {

    public ResultCode resultCode;

    public BusinessException(ResultCode errorCode) {
        super(errorCode.getMsg());
        this.resultCode = errorCode;
    }

    public BusinessException(String message) {
        super(message);
    }

}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

在全局异常处理器中添加业务异常处理逻辑

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 处理自定义业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public <T> Result<T> handleBusinessException(BusinessException e) {
        log.error(e.getMessage(), e);
        if(e.getResultCode()!=null){
            return Result.failed(e.getResultCode());
        }
        return Result.failed(e.getMessage());
    }

}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

模拟业务异常

    @Operation(summary = "获取用户详情")
    @GetMapping("/{id}")
    public Result<SysUser> getUserById(
            @Parameter(description = "用户ID") @PathVariable Long id
    ) {
        SysUser user = userService.getById(-1);
        // 模拟异常
        if (user == null) {
            throw new BusinessException("用户不存在");
        }
        return Result.success(user);
    }

 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

请求不存在的用户时,响应如下:

通过全局异常处理的引入和自定义业务异常的定义,接口的响应数据得以标准化,提升了前后端协作的效率和系统的可维护性。

日志输出配置

日志作为企业级应用项目中的重要一环,不仅是调试问题的关键手段,更是用户问题排查和争议解决的强有力支持工具

配置 logback-spring.xml 日志文件

src/main/resources 目录下,新增 logback-spring.xml 配置文件。基于 Spring Boot “约定优于配置” 的设计理念,项目默认会自动加载并使用该配置文件。



<configuration>

    
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
    <property name="LOG_HOME" value="/logs/${APP_NAME}"/>

    
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        
        
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUGlevel>
        filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}Pattern>
            <charset>UTF-8charset>
        encoder>
    appender>

    
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        
        <file>${LOG_HOME}/log.logfile>
        <encoder>
            
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} -%5level ---[%15.15thread] %-40.40logger{39} : %msg%n%npattern>
            <charset>UTF-8charset>
        encoder>
        
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            
            <fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}.%i.logfileNamePattern>
            
            <maxFileSize>10MBmaxFileSize>
            
            <maxHistory>30maxHistory>
            
            <totalSizeCap>1GBtotalSizeCap>
        rollingPolicy>
        
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFOlevel>
        filter>
    appender>

    
    <root level="INFO">
        
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    root>
configuration>
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

查看日志输出效果

添加配置文件后,启动项目并触发相关日志行为,控制台和日志文件会同时输出日志信息:

集成 Spring Security

Spring Security 是一个强大的安全框架,可用于身份认证和权限管理。

添加依赖

pom.xml 添加 Spring Security 依赖

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-securityartifactId>
dependency>
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

获取用户认证信息

从数据库获取用户信息(用户名、密码、角色),用于和前端输入的用户名密码做判读,如果认证成功,将角色权限信息绑定到用户会话,简单概括就是提供给认证授权的用户信息。

定义用户认证信息类 UserDetails

创建 com.youlai.boot.security.model 包,新建 SysUserDetails 用户认证信息对象,继承 Spring Security 的 UserDetails 接口

/**
 * Spring Security 用户认证信息对象
 * 

* 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 * * @author youlai */ @Data @NoArgsConstructor public class SysUserDetails implements UserDetails { /** * 用户ID */ private Integer userId; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 账号是否启用(true:启用,false:禁用) */ private Boolean enabled; /** * 用户角色权限集合 */ private Collection<SimpleGrantedAuthority> authorities; /** * 根据用户认证信息初始化用户详情对象 */ public SysUserDetails(SysUser user) { this.userId = user.getId(); this.username = user.getUsername(); this.password = user.getPassword(); this.enabled = ObjectUtil.equal(user.getStatus(), 1); // 初始化角色权限集合 this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) ? user.getRoles().stream() // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (sys:user:add) .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toSet()) : Collections.emptySet(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isEnabled() { return this.enabled; } } class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

获取用户认证信息服务类

创建 com.youlai.boot.security.service 包,新建 SysUserDetailsService 用户认证信息加载服务类,继承 Spring Security 的 UserDetailsService 接口

/**
 * 用户认证信息加载服务类
 * 

* 在用户登录时,Spring Security 会自动调用该类的 {@link #loadUserByUsername(String)} 方法, * 获取封装后的用户信息对象 {@link SysUserDetails},用于后续的身份验证和权限管理。 * * @author youlai */ @Service @RequiredArgsConstructor public class SysUserDetailsService implements UserDetailsService { private final SysUserService userService; /** * 根据用户名加载用户的认证信息 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户基本信息 SysUser user = userService.getOne(new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, username) ); if (user == null) { throw new UsernameNotFoundException(username); } // 模拟设置角色,实际应从数据库获取用户角色信息 Set<String> roles = Set.of("ADMIN"); user.setRoles(roles); // 模拟设置权限,实际应从数据库获取用户权限信息 Set<String> perms = Set.of("sys:user:query"); user.setPerms(perms); // 将数据库中查询到的用户信息封装成 Spring Security 需要的 UserDetails 对象 return new SysUserDetails(user); } } class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

认证鉴权异常处理

com.youlai.boot.common.util 添加响应工具类 ResponseUtils

@Slf4j
public class ResponseUtils {

    /**
     * 异常消息返回(适用过滤器中处理异常响应)
     *
     * @param response  HttpServletResponse
     * @param resultCode 响应结果码
     */
    public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
        // 根据不同的结果码设置HTTP状态
        int status = switch (resultCode) {
            case ACCESS_UNAUTHORIZED, 
            	ACCESS_TOKEN_INVALID 
                    -> HttpStatus.UNAUTHORIZED.value();
            default -> HttpStatus.BAD_REQUEST.value();
        };

        response.setStatus(status);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());

        try (PrintWriter writer = response.getWriter()) {
            String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));
            writer.print(jsonResponse);
            writer.flush(); // 确保将响应内容写入到输出流
        } catch (IOException e) {
            log.error("响应异常处理失败", e);
        }
    }

}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">
class="table-box">
功能AuthenticationEntryPointAccessDeniedHandler
对应异常AuthenticationExceptionAccessDeniedException
适用场景用户未认证(无凭证或凭证无效)用户已认证但无权限
返回 HTTP 状态码401 Unauthorized403 Forbidden
常见使用位置用于处理身份认证失败的全局入口逻辑用于处理权限不足时的逻辑

用户未认证处理器

/**
 * 未认证处理器
 *
 * @author youlai
 */
@Slf4j
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
        if (authException instanceof BadCredentialsException) {
            // 用户名或密码错误
            ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR);
        } else {
            // token 无效或者 token 过期
            ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
        }
    }
    
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

无权限访问处理器

/**
 * 无权限访问处理器
 *
 * @author youlai
 */
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
        ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);
    }

}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

注意事项

在全局异常处理器中,认证异常(AuthenticationException)和授权异常(AccessDeniedException)不应被捕获,否则这些异常将无法交给 Spring Security 的异常处理机制进行处理。因此,当捕获到这类异常时,应该将其重新抛出,交给 Spring Security 来处理其特定的逻辑。

public class GlobalExceptionHandler {

    /**
     * 处理系统异常
     * 

* 兜底异常处理,处理未被捕获的异常 */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public <T> Result<T> handleNullPointerException(Exception e) throws Exception { // 如果是 Spring Security 的认证异常或授权异常,直接抛出,交由 Spring Security 的异常处理器处理 if (e instanceof AccessDeniedException || e instanceof AuthenticationException) { throw e; } log.error(e.getMessage(), e); return Result.failed("系统异常,请联系管理员"); } } class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

认证授权配置

com.youlai.boot.config 包下新建 SecurityConfig 用来 Spring Security 安全配置

/**
 * Spring Security 安全配置
 *
 * @author youlai
 */
@Configuration
@EnableWebSecurity  // 启用 Spring Security 的 Web 安全功能,允许配置安全过滤链
@EnableMethodSecurity // 启用方法级别的安全控制(如 @PreAuthorize 等)
public class SecurityConfig {

    /**
     * 忽略认证的 URI 地址
     */
    private final String[] IGNORE_URIS = {"/api/v1/auth/login"};

    /**
     * 配置安全过滤链,用于定义哪些请求需要认证或授权
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // 配置认证与授权规则
        http
                .authorizeHttpRequests(requestMatcherRegistry ->
                        requestMatcherRegistry
                                .requestMatchers(IGNORE_URIS).permitAll() // 登录接口无需认证
                                .anyRequest().authenticated() // 其他请求必须认证
                )
                // 使用无状态认证,禁用 Session 管理(前后端分离 + JWT)
                .sessionManagement(configurer ->
                        configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                // 禁用 CSRF 防护(前后端分离通过 Token 验证,不需要 CSRF)
                .csrf(AbstractHttpConfigurer::disable)
                // 禁用默认的表单登录功能
                .formLogin(AbstractHttpConfigurer::disable)
                // 禁用 HTTP Basic 认证(统一使用 JWT 认证)
                .httpBasic(AbstractHttpConfigurer::disable)
                // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中
                .headers(headers ->
                        headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)
                )
                // 异常处理
                .exceptionHandling(configurer -> {
                    configurer
                            .authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证处理器
                            .accessDeniedHandler(new MyAccessDeniedHandler()); // 无权限访问处理器
                });
            
            ;

        return http.build();
    }

    /**
     * 配置密码加密器
     *
     * @return 密码加密器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
     /**
     * 用于配置不需要认证的 URI 地址
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> {
            web.ignoring().requestMatchers(
                    "/v3/api-docs/**",
                    "/swagger-ui/**",
                    "/swagger-ui.html",
                    "/webjars/**",
                    "/doc.html"
            );
        };
    }

    /**
     *认证管理器
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }
}

 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

Token 工具类

com.youlai.boot.security.manager 包下新建 JwtTokenManager ,用于生成和解析 token

/**
 * JWT Token 管理类
 *
 * @author youlai
 */
@Service
public class JwtTokenManager {

    /**
     * JWT 密钥,用于签名和解签名
     */
    private final String secretKey = " SecretKey012345678901234567890123456789012345678901234567890123456789";

    /**
     * 访问令牌有效期(单位:秒), 默认 1 小时
     */
    private final Integer accessTokenTimeToLive = 3600;

    /**
     *  生成 JWT 访问令牌 - 用于登录认证成功后生成 JWT Token
     *
     * @param authentication 用户认证信息
     * @return JWT 访问令牌
     */
    public String generateToken(Authentication authentication) {
        SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
        Map<String, Object> payload = new HashMap<>();
        // 将用户 ID 放入 JWT 载荷中, 如有其他扩展字段也可以放入
        payload.put("userId", userDetails.getUserId());

        // 将用户的角色和权限信息放入 JWT 载荷中,例如:["ROLE_ADMIN", "sys:user:query"]
        Set<String> authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toSet());
        payload.put("authorities", authorities);

        Date now = new Date();
        payload.put(JWTPayload.ISSUED_AT, now);

        // 设置过期时间 -1 表示永不过期
        if (accessTokenTimeToLive != -1) {
            Date expiresAt = DateUtil.offsetSecond(now, accessTokenTimeToLive);
            payload.put(JWTPayload.EXPIRES_AT, expiresAt);
        }
        payload.put(JWTPayload.SUBJECT, authentication.getName());
        payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());

        return JWTUtil.createToken(payload, secretKey.getBytes());
    }


    /**
     * 解析 JWT Token 获取 Authentication 对象 - 用于接口请求时解析 JWT Token 获取用户信息
     *
     * @param token JWT Token
     * @return Authentication 对象
     */
    public Authentication parseToken(String token) {

        JWT jwt = JWTUtil.parseToken(token);
        JSONObject payloads = jwt.getPayloads();
        SysUserDetails userDetails = new SysUserDetails();
        userDetails.setUserId(payloads.getInt("userId")); // 用户ID
        userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
        // 角色集合
        Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray("authorities")
                .stream()
                .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
                .collect(Collectors.toSet());

        return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
    }

    /**
     *  验证 JWT Token 是否有效
     *
     * @param token JWT Token 不携带 Bearer 前缀
     * @return 是否有效
     */
    public boolean validateToken(String token) {
        JWT jwt = JWTUtil.parseToken(token);
        // 检查 Token 是否有效(验签 + 是否过期)
        return jwt.setKey(secretKey.getBytes()).validate(0);
    }

}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

登录认证接口

com.youlai.boot.controller 包下新建 AuthController

/**
 * 认证控制器
 *
 * @author youlai
 */
@Tag(name = "01.认证中心")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

    // 认证管理器 - 用于执行认证
    private final AuthenticationManager authenticationManager;

    // JWT 令牌服务类 - 用于生成 JWT 令牌
    private final JwtTokenManager jwtTokenManager;

    @Operation(summary = "登录")
    @PostMapping("/login")
    public Result<String> login(
            @Parameter(description = "用户名", example = "admin") @RequestParam String username,
            @Parameter(description = "密码", example = "123456") @RequestParam String password
    ) {

        // 1. 创建用于密码认证的令牌(未认证)
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username.trim(), password);

        // 2. 执行认证(认证中)
        Authentication authentication = authenticationManager.authenticate(authenticationToken);

        // 3. 认证成功后生成 JWT 令牌(已认证)
        String accessToken = jwtTokenManager.generateToken(authentication);

        return Result.success(accessToken);
    }
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

访问本地接口文档 http://localhost:8080/doc.html 选择登录接口进行调试发送请求,输入用户名和密码,如果登录成功返回访问令牌 token

image-20241222162606665

访问 https://jwt.io/ 解析返回的 token ,主要分为三部分 Header(头部) 、Payload(负载) 和 Signature(签名) ,其中负载除了固定字段之外,还出现自定义扩展的字段 userId。

访问鉴权

我们拿获取用户列表举例,首先需要验证我们在上一步登录拿到的访问令牌 token 是否有效(验签、是否过期等),然后需要校验该用户是否有访问接口的权限,本节就围绕以上问题展开。

验证解析 Token 过滤器

新建 com.youlai.boot.security.filter 添加 JwtValidationFilter 过滤器 用于验证和解析token

/**
 * JWT Token 验证和解析过滤器
 * 

* 负责从请求头中获取 JWT Token,验证其有效性并将用户信息设置到 Spring Security 上下文中。 * 如果 Token 无效或解析失败,直接返回错误响应。 *

* * @author youlai */
public class JwtAuthenticationFilter extends OncePerRequestFilter { private static final String BEARER_PREFIX = "Bearer "; private final JwtTokenManager jwtTokenManager; public JwtAuthenticationFilter(JwtTokenManager jwtTokenManager) { this.jwtTokenManager = jwtTokenManager; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader(HttpHeaders.AUTHORIZATION); try { if (StrUtil.isNotBlank(token) && token.startsWith(BEARER_PREFIX)) { // 去除 Bearer 前缀 token = token.substring(BEARER_PREFIX.length()); // 校验 JWT Token ,包括验签和是否过期 boolean isValidate = jwtTokenService.validateToken(token); if (!isValidate) { writeErrMsg(response, ResultCode.TOKEN_INVALID); return; } // 将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中 Authentication authentication = jwtTokenManager.parseToken(token); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { SecurityContextHolder.clearContext(); writeErrMsg(response, ResultCode.TOKEN_INVALID); return; } // 无 Token 或 Token 验证通过时,继续执行过滤链。 // 如果请求不在白名单内(例如登录接口、静态资源等), // 后续的 AuthorizationFilter 会根据配置的权限规则和安全策略进行权限校验。 // 例如: // - 匹配到 permitAll() 的规则会直接放行。 // - 需要认证的请求会校验 SecurityContext 中是否存在有效的 Authentication。 // 若无有效 Authentication 或权限不足,则返回 403 Forbidden。 filterChain.doFilter(request, response); } /** * 异常消息返回 * * @param response HttpServletResponse * @param resultCode 响应结果码 */ public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { int status = switch (resultCode) { case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); default -> HttpStatus.BAD_REQUEST.value(); }; response.setStatus(status); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); try (PrintWriter writer = response.getWriter()) { String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); writer.print(jsonResponse); writer.flush(); } catch (IOException e) { // 日志记录:捕获响应写入失败异常 // LOGGER.error("Error writing response", e); } } } class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

添加 JWT 验证和解析过滤器

在 SecurityConfig 过滤器链添加 JWT token校验和解析成 Authentication 对象的过滤器。

/**
 * Spring Security 安全配置
 *
 * @author youlai
 */
@RequiredArgsConstructor
public class SecurityConfig {

    // JWT Token 服务 , 用于 Token 的生成、解析、验证等操作
    private final JwtTokenManager jwtTokenManager;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      	return http 
                // ... 
                // JWT 验证和解析过滤器
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class)
		  .build();
    }

    // ...
}

 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

获取用户列表接口

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final SysUserService userService;

    @Operation(summary = "获取用户列表")
    @GetMapping
    @PreAuthorize("hasAuthority('sys:user:query')")
    public List<SysUser> listUsers() {
        return userService.list();
    }
    
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

访问一个主要测试访问凭据令牌是否认证以及对应的用户是否有访问该接口所需的权限,上面获取用户信息列表的接口未配置在security的白名单中,也就是需要认证,且被 @PreAuthorize(“hasAuthority(‘sys:user:query’)”) 标记说明用户需要有 sys:user:query的权限,也就是所谓的鉴权。

正常访问

image-20241224175350088

不携带 token 访问

携带错误/过期的 token

有访问权限

用户拥有的权限 sys:user:query

public class UserController {

    @Operation(summary = "获取用户列表")
    @GetMapping
    @PreAuthorize("hasAuthority('sys:user:query')")  // 需要 sys:user:query 权限
    public List<SysUser> listUsers() {
        return userService.list();
    }
    
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

在这里插入图片描述

无访问权限

用户没有拥有的权限 sys:user:info

public class UserController {

    @Operation(summary = "获取用户详情")
    @GetMapping("/{id}")
    @PreAuthorize("hasAuthority('sys:user:info')") // 需要 sys:user:info 权限
    public Result<SysUser> getUserById(
            @Parameter(description = "用户ID") @PathVariable Long id
    ) {
        SysUser user = userService.getById(id);
    }     
}
 class="hljs-button signin active" data-title="登录复制" data-report-click="{"spm":"1001.2101.3001.4334"}">

image-20241225000802438

结语

通过本文,您将了解企业级后端开发的核心技能和项目从搭建到部署的完整流程。如有兴趣,欢迎访问开源项目:https://gitee.com/youlaiorg,关注公众号【有来技术】,或添加微信(微信号:haoxianrui)参与开源交流。

id="blogExtensionBox" style="width:400px;margin:auto;margin-top:12px" class="blog-extension-box"> class="blog_extension blog_extension_type1" id="blog_extension"> class="blog_extension_card" data-report-click="{"spm":"1001.2101.3001.6470"}" data-report-view="{"spm":"1001.2101.3001.6470"}"> class="blog_extension_card_left"> class="blog_extension_card_cont"> class="blog_extension_card_cont_l"> 有来技术 class="blog_extension_card_cont_r"> 微信公众号 无广告,佛系公众号,随缘更新团队成员技术
注:本文转载自blog.csdn.net的斯坦福的兔子的文章"https://blog.csdn.net/weixin_41966507/article/details/122827114"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接

评论记录:

未查询到任何数据!