全栈开发之后端脚手架:SpringBoot集成MybatisPlus代码生成,分页,雪花算法,统一响应,异常拦截,Swagger3接口文档

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS MySQL DuckDB 分析主实例,基础系列 4核8GB
RDS DuckDB + QuickBI 企业套餐,8核32GB + QuickBI 专业版
简介: 全栈开发之后端脚手架:SpringBoot集成MybatisPlus代码生成,分页,雪花算法,统一响应,异常拦截,Swagger3接口文档

背景


当需要快速实现一个想法时,如果采用 Java 技术栈,一般都是选择 SpringBoot 技术栈,虽然 SpringBoot 解决了传统 SpringMVC 配置等方面的问题,且其生态体系也非常强大,但是在实际使用时仍然需要集成最起码的数据库、响应封装、异常拦截、代码生成器、接口文档等基础组件,这时一般有两种手段:


  1. 使用开源的各类后台管理系统,这类系统一般模块完善,功能强大;然而很多无关紧要的模块又会带来一些干扰;


  1. 使用自建的简化版,集成最常用的模块即可,一方面便于掌控,同时在“重复造轮子”的过程中也熟悉下这些主流技术是如何整合在一起的。


这里,就从零开始搭建后端脚手架,以搭积木的方式将开源组件组装起来。后续的玩具项目都基于这个脚手架进行开发。


MybatisPlus代码生成器


参考官方的代码仓库以及文档mp.baomidou.com/guide/gener…

image.png

简单修改路径信息后,直接执行 MysqlGenerator 类的 main 方法。分别键入模块表以及表名即可生成 Entity , Mapper , Service , Controller 等对应的文件。


MybatisPlus雪花算法ID


image.png

MybatisPlus (3.3.1)的主键策略默认是雪花算法,如果不显式设置主键的话, MybatisPlus 通过代码自动通过雪花算法算出一个值,插入的时候就会将其作为id插入。


雪花算法( SnowFlake )是一个 Long 类型的 Java 长整型数字,一般对应 MySQL 中的类型为 BIGINT(20) ;具有趋势单调递增,且全局唯一的特点。


MybatisPlus分页


image.png

  • MybatisPlusConfig.java
@Configuration
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor().setCountSqlParser(new JsqlParserCountOptimize(true));
    }
}
  • PageUtils.java
@Data
public class PageUtils {
    //总记录数
    private long total;
    //每页记录数
    private long size;
    //总页数
    private long pages;
    //当前页数
    private long current;
    //列表数据
    private List<?> records;
    //灵活添加
    private Map<String,Object> data;
    /**
     * 分页
     * @param records        列表数据
     * @param total  总记录数
     * @param size    每页记录数
     * @param current    当前页数
     */
    public PageUtils(List<?> records, long total, long size, long current) {
        this.records = records;
        this.total = total;
        this.size = size;
        this.current = current;
        this.pages = (long)Math.ceil((double)total/size);
    }
    /**
     * 分页
     * @param records        列表数据
     * @param total  总记录数
     * @param size    每页记录数
     * @param current    当前页数
     */
    public PageUtils(List<?> records, long total, long size, long current, Map<String,Object> data) {
        this.records = records;
        this.total = total;
        this.size = size;
        this.current = current;
        this.data = data;
        this.pages = (long)Math.ceil((double)total/size);
    }
    /**
     * 分页
     */
    public PageUtils(Page<?> page) {
        this.records = page.getRecords();
        this.total = (long)page.getTotal();
        this.size = page.getSize();
        this.current = page.getCurrent();
        this.pages = (long)page.getPages();
    }
}
  • BookController.java
// 分页查询:使用自定义PageUtils
@GetMapping("list")
public Result<PageUtils> list(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam Map<String, Object> params) {
    PageUtils list = bookService.findList(new Page<>(page, size), params);
    return Result.success(list);
}
// 分页查询:使用MyBatisPlus的page方法
@GetMapping("page")
public Result<IPage<Book>> page(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam Map<String, Object> params) {
    QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
    queryWrapper.likeRight("read_date", params.get("readDate"));
    IPage<Book> list = bookService.page(new Page<>(page, size),queryWrapper);
    return Result.success(list);
}


统一响应封装


image.png

通过 RestControllerAdvice 注解,实现对请求的拦截,统一封装结果为 Result

@RestControllerAdvice
public class ResultAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (o instanceof String) {
            return objectMapper.writeValueAsString(Result.success(o));
        }
        if (o instanceof Result) {
            return o;
        }
        return Result.success(o);
    }
}
  • Result.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    /** 结果状态 ,正常响应200,其他状态码都为失败*/
    private int code;
    private String msg;
    private T data;
    // Static methods
    /**
     * 成功时候的调用
     */
    public static <T> Result<T> success(T data) {
        return new Result<T>(data, CodeMsg.SUCCESS);
    }
    public static <T> Result<T> success() {
        return new Result<T>(CodeMsg.SUCCESS);
    }
    /**
     * 失败时候的调用
     */
    public static <T> Result<T> error(Integer code, String msg) {
        return new Result<T>(code, msg);
    }
    public static <T> Result<T> error(CodeMsg codeMsg) {
        return new Result<T>(codeMsg);
    }
    public static <T> Result<T> error(String msg) {
        CodeMsg codeMsg = new CodeMsg(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg);
        return new Result<T>(codeMsg);
    }
    // Constructor
    private Result(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    private Result(T data, CodeMsg codeMsg) {
        this.data = data;
        if (codeMsg != null) {
            this.code = codeMsg.getCode();
            this.msg = codeMsg.getMsg();
        }
    }
    private Result(CodeMsg codeMsg) {
        if (codeMsg != null) {
            this.code = codeMsg.getCode();
            this.msg = codeMsg.getMsg();
        }
    }
}
  • CodeMsg.java
@Getter
public class CodeMsg {
    private int code;
    private String msg;
    // 通用的错误码
    public static final CodeMsg SUCCESS =new CodeMsg(HttpStatus.OK.value(), "success");
    public static final CodeMsg BAD_REQUEST = new CodeMsg(HttpStatus.BAD_REQUEST.value(), "请求无效");
    public static final CodeMsg SERVER_ERROR = new CodeMsg(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务端异常");
    public static final CodeMsg NO_HANDLER_FOUND = new CodeMsg(HttpStatus.NOT_FOUND.value(), "未找到对应资源");
    public static final CodeMsg UNAUTHORIZED = new CodeMsg(HttpStatus.UNAUTHORIZED.value(), "未认证或登录状态过期");
    public static final CodeMsg FORBIDDEN = new CodeMsg(HttpStatus.FORBIDDEN.value(), "未授权");
    // 自定义错误码
    public static final CodeMsg PARAMETER_ERROR = new CodeMsg(4000, "参数不正确!");
    /*用户相关:验证码*/
    public static final CodeMsg CAPTCHA_EXPIRED = new CodeMsg(4001, "验证码不存在或已过期");
    public static final CodeMsg CAPTCHA_INVALID = new CodeMsg(4002, "验证码错误");
    /*用户相关:认证授权*/
    public static final CodeMsg BAD_CREDENTIAL = new CodeMsg(4003, "用户名或密码错误");
    public static final CodeMsg ACCOUNT_NOT_FOUND = new CodeMsg(4004, "账号不存在");
    public static final CodeMsg ACCOUNT_NOT_ACTIVATED = new CodeMsg(4005, "账号未激活");
    // 限流
    public static final CodeMsg RATE_LIMIT = new CodeMsg(4006,"达到阈值啦!");
    // 熔断
    public static final CodeMsg DEGRADE = new CodeMsg(4007,"熔断啦!");
    public static CodeMsg error(String msg){
        return new CodeMsg(HttpStatus.BAD_REQUEST.value(),msg);
    }
    public CodeMsg(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}


全局异常拦截


image.png

默认拦截所有异常(也可自定义异常进行封装),同样通过 RestControllerAdvice 注解,实现对异常响应的统一封装。


  • RestExceptionHandler.java
@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<String> exception(Exception e) {
        log.error("Global exception: {}", null == e.getMessage() ? e.toString() : e.getMessage(), e);
        return Result.error(CodeMsg.SERVER_ERROR.getCode(), null == e.getMessage() ? e.toString() : e.getMessage());
    }
}


CRUD的Controller


@RestController
@RequestMapping("book")
@Api(tags = "测试Controller")
public class BookController {
    @Autowired
    IBookService bookService;
    @GetMapping("hello")
    @ApiOperation("哈喽")
    public String hello() {
        return "hello everyone.";
    }
    @GetMapping("list")
    public List<Book> list() {
        return bookService.list();
    }
    @PostMapping("save")
    public boolean save(@RequestBody Book book) {
        return bookService.save(book);
    }
    @GetMapping("detail/{id}")
    public Result detail(@PathVariable long id) {
        return Result.success(bookService.getById(id));
    }
    @GetMapping("error")
    public Result error() {
        int value = 8 / 0;
        return Result.success(value);
    }
    @GetMapping("page")
    public Result<IPage<Book>> page(@RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam Map<String, Object> params) {
        QueryWrapper<Book> queryWrapper = new QueryWrapper<>();
        queryWrapper.likeRight("read_date", params.get("readDate"));
        IPage<Book> list = bookService.page(new Page<>(page, size),queryWrapper);
        return Result.success(list);
    }
}


Swagger3接口文档


image.png

  • 引入依赖
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>${swagger.version}</version>
</dependency>
  • 配置类
@Configuration
@EnableOpenApi
public class SwaggerConfig {
    private static final String VERSION = "1.0.0";
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.heartsuit.readingnotes.controller"))
                .paths(PathSelectors.any())
                .build();
    }
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("SpringBoot+Swgger3.0后端服务接口文档")
                .contact(new Contact("Heartsuit", "https://blog.csdn.net/u013810234", "454670286@qq.com"))
                .description("基于Swagger3.0生成的接口文档")
                .termsOfServiceUrl("https://blog.csdn.net/u013810234")
                .license("The Apache License, Version 2.0")
                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
                .version(VERSION)
                .build();
    }
}
  • 在控制器以及接口上添加注解
@Api(tags = "测试Controller")
@RestController
public class HelloController {
    @GetMapping("hello")
    @ApiOperation("哈喽")
    public String hello() {
        return "Hello SpringBoot with Swagger3.0";
    }
}
  • 启动服务,浏览器访问


没错,再没其他额外的注解了,直接启动服务,然后在浏览器访问即可。


Note:



实际中我们的接口文档只会在开发环境下使用,所以一般我们会在生产环境下关闭文档。


  • application.yml
spring:
  profiles:
    active: dev
  • application-dev.yml
springfox:
  documentation:
    enabled: true
  • application-prod.yml
springfox:
  documentation:
    enabled: false


遇到的问题


  • 问题1:控制台打印MyBatisPlusSQL日志


解决方法:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 问题2:Long类型的雪花算法ID传到前端后精度丢失


解决方法:在后端 JSON 返回前统一将 Long 转为字符串。

@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        return objectMapper;
    }
}
  • 问题3:全局异常处理时多个异常处理器匹配顺序


解决方法:


如下,除了全局拦截的所有异常 Exception 之外,还有一个自定义的异常 CustomException ,那么,当出现 CustomException 时,当前两个异常该如何匹配呢?答案是子类异常处理器优先,即会被 customException 方法拦截,而不会被 exception 方法拦截。

@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<String> exception(Exception e) {
        log.error("Global exception: {}", null == e.getMessage() ? e.toString() : e.getMessage(), e);
        return Result.error(CodeMsg.SERVER_ERROR.getCode(), null == e.getMessage() ? e.toString() : e.getMessage());
    }
    @ExceptionHandler(CustomException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Result<String> customException(CustomException e) {
        log.error("Custom exception: {}", null == e.getMessage() ? e.toString() : e.getMessage(), e);
        return Result.error(e.getCode(), null == e.getMessage() ? e.toString() : e.getMessage());
    }
}
@Getter
public class CustomException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    private Integer code;
    public CustomException(CodeMsg codeMsg) {
        super(codeMsg.getMsg());
        this.code = codeMsg.getCode();
    }
    public CustomException(Integer code, String msg){
        super(msg);
        this.code = code;
    }
}
  • 问题4:访问Swagger地址时报错:Unable to infer base url. This is common when using dynamic servlet registration or when the API is behind an API Gateway. The base url is the root of where all the swagger resources are served. For e.g. if the api is available at example.org/api/v2/api-… then the base url is example.org/api/. Please enter the location manually

解决方法:


原因是我们使用 RestControllerAdvice 统一处理接口响应,导致给Swagger的返回值也包装了一层,最终在浏览器无法解析、渲染页面。


@RestControllerAdvice 改为: @RestControllerAdvice(basePackages = "com.heartsuit.*.controller")

即限制 RestControllerAdvice 的拦截范围,仅处理指定包下的接口响应。


项目依赖


<properties>
        <java.version>11</java.version>
        <mybatisplus.version>3.3.1</mybatisplus.version>
        <swagger.version>3.0.0</swagger.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--Web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--MySQL and ORM-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.21</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatisplus.version}</version>
        </dependency>
        <!--Swagger3.0-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>


配置文件


server:
  port: 8000
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      url: jdbc:mysql://localhost:3306/reading_notes?serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useSSL=false
      username: root
      password: root
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  typeAliasesPackage: com.heartsuit.*.entity
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl


IDEA相关插件


image.png

用到的插件:


  • Lombok :通过注解生成Getter, Setter, toString()以及日志打印;
  • MyBatis Log Plugin :从MyBatis以及MyBatisPlus控制台日志的SQL复原SQL+参数拼接;
  • RestfulToolkit :在IDEA中测试控制层的接口,无需再切换出IDE到浏览器或者Postman;
  • Free Mybatis plugin :链接Mapper接口与xml;


相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
目录
相关文章
|
8月前
|
安全 Java Ruby
我尝试了所有后端框架 — — 这就是为什么只有 Spring Boot 幸存下来
作者回顾后端开发历程,指出多数框架在生产环境中难堪重负。相比之下,Spring Boot凭借内置安全、稳定扩展、完善生态和企业级支持,成为构建高可用系统的首选,真正经受住了时间与规模的考验。
644 2
|
JavaScript 前端开发 Java
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
1145 1
|
8月前
|
SQL Java 数据库连接
MyBatis分页
MyBatis作为Java持久层框架,需结合数据库特性或插件实现分页。分页分为物理分页(如MySQL的LIMIT)和逻辑分页(内存截取),推荐使用PageHelper插件自动注入分页语句,提升开发效率与性能。需注意索引优化、深分页问题及多表关联时的兼容性,结合业务场景选择合适方案。
301 4
|
SQL Java 数据库连接
微服务——MyBatis分页
本文介绍了分页的多种实现方式,包括自带RowBounds分页、第三方插件PageHelper分页、SQL分页、数组分页及拦截器分页。其中,RowBounds是先查询全部结果再内存分页;PageHelper通过修改SQL动态添加分页关键字;SQL分页依赖数据库自身的分页功能如`LIMIT`;数组分页则是查询全量数据后用`subList`方法截取;拦截器分页则统一在SQL后添加分页语句。最后总结逻辑分页适合小数据量,但大数据量易内存溢出;物理分页虽小数据量效率较低,但更适合大数据场景,优先推荐使用。
187 0
|
SQL Oracle 关系型数据库
【YashanDB知识库】Mybatis-Plus调用YashanDB怎么设置分页
【YashanDB知识库】Mybatis-Plus调用YashanDB怎么设置分页
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
699 70
|
12月前
|
Java 数据库连接 微服务
若依微服务的Mybatis-plus集成过程:一份详细的入门教程。
以上就是Spring Boot项目中集成MyBatis Plus的详细步骤。集成成功后,你就可以使用Mybatis-plus提供的强大功能,让你的增删改查操作更为简单。以上步骤简单易懂,非常适合初学者使用。希望对您有所帮助。
1048 20
|
12月前
|
SQL Java 数据安全/隐私保护
发现问题:Mybatis-plus的分页总数为0,分页功能失效,以及多租户插件的使用。
总的来说,使用 Mybatis-plus 确实可以极大地方便我们的开发,但也需要我们理解其工作原理,掌握如何合适地使用各种插件。分页插件和多租户插件是其中典型,它们的运用可以让我们的代码更为简洁、高效,理解和掌握好它们的用法对我们的开发过程有着极其重要的意义。
1051 15
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
本文介绍了在Spring Boot项目中如何通过创建`GlobalExceptionHandler`类来全局处理系统异常。通过使用`@ControllerAdvice`注解,可以拦截项目中的各种异常,并结合`@ExceptionHandler`注解针对特定异常(如参数缺失、空指针等)进行定制化处理。文中详细展示了处理参数缺失异常和空指针异常的示例代码,并说明了通过拦截`Exception`父类实现统一异常处理的方法。虽然拦截`Exception`可一劳永逸,但为便于问题排查,建议优先处理常见异常,最后再兜底处理未知异常,确保返回给调用方的信息友好且明确。
1509 0
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
620 0