SpringBoot中如何优雅的统一全局返回格式与处理系统异常?

news2025/2/22 13:43:38

在领域驱动设计(DDD)中,接口层主要负责处理与外部系统的交互,包括接收用户或外部系统的请求,调用应用层服务处理请求,以及将处理结果返回给请求方。

我发现一些代码中,接口的返回值类型众多,有的直接返回数据传输对象(DTO),甚至直接返回数据对象(DO),还有的返回Result对象。在DailyMart项目中,为了简化客户端的处理流程,我们决定在接口层采用统一的返回格式——Result对象。

1. 统一返回格式

1.1 构建Result对象

为了实现统一返回格式,我们在DailyMart项目中构建了一个Result对象,代码如下:

@Data
@Accessors(chain = true)
public class Result<T> {
    public static final String SUCCESS_CODE = "OK";
    private String code;
    private String message;
    private T data;
    private long timestamp;
}

为了便于创建Result对象,我们构建了一个辅助类ResultHelper:

@Slf4j
public class ResultHelper {

    public static <T> Result<T> success(T data) {
        return new Result<T>()
                .setCode(SUCCESS_CODE)
                .setData(data)
                .setTimestamp(System.currentTimeMillis());

    }

    public static <T> Result<T> fail(String message) {
        return new Result<T>()
                .setCode(ErrorCode.SERVICE_ERROR.getCode())
                .setMessage(message)
                .setTimestamp(System.currentTimeMillis());

    }
...
}

1.2 优化DailyMart中的接口

以DailyMart系统的注册接口为例,定义了Result对象后,我们可以在接口层这样优化代码:

@PostMapping("/api/customer/register")
public Result<UserRegistrationDTO> register(@RequestBody @Valid UserRegistrationDTO customerDTO){
 try {
  return ResultHelper.success(customerService.register(customerDTO));
 }catch (Exception e){
  return ResultHelper.fail(e.getMessage());
 }
}

为了避免每个接口都这样写,我们可以利用SpringBoot的全局异常处理器来处理,这将在下一节讨论。

1.3 优化后的结果

现在,当访问注册接口时,成功会返回如下响应:

{
    "code": "OK",
    "message": null,
    "data": {
        "userName": "jianzh1",
        "password": null,
        "email": "jianzh5@162.com",
        "phone": "18811117882"
    },
    "timestamp": 1687338445851
}

失败时会返回如下响应:

{
    "code": "B0001",
    "message": "用户已存在",
    "data": null,
    "timestamp": 1687338319457
}

这样,我们成功地实现了接口层的返回格式的统一。

2. 异常控制

在DailyMart的代码实现中,我们通常会在遇到问题时抛出RuntimeException。例如,在用户登录时,如果用户不存在,我们会抛出一个RuntimeException:

@Override
protected CustomerUser authenticate(UserLoginDTO loginDTO) {
 CustomerUser actualUser = customerUserRepository.findByUserName(usernamePasswordLoginDTO.getUsername());
 
 if(actualUser == null){
  throw new RuntimeException("用户不存在");
 }
 return actualUser;
}

然而,在构建大型系统时,通常建议使用自定义异常来替代RuntimeException。自定义异常可以提供更精细和具有针对性的错误信息,有助于区分系统中的不同类型的错误。使用自定义异常不仅可以提高代码的可读性,因为它们的名称和内容可以直接反映出问题的性质,而且还可以包含更多的信息,比如错误码或其他相关的上下文数据。

2.1 错误码的概念与应用

在开发过程中,错误码的使用是提升异常处理可读性和效率的有效手段。根据《阿里巴巴开发规范-黄山版》,错误码的制定和使用应遵循一定的原则,以便实现快速溯源和标准化沟通。

错误码的组成

错误码通常是一个包含5个字符的字符串,它分为两部分:错误来源标识(1个字符)和错误编号(4个数字)。错误来源标识可以是ABC

  • A 表示错误源于用户,例如参数错误、版本过低或支付超时。

  • B 表示错误源于当前系统,通常是由于业务逻辑错误或程序健壮性不足。

  • C 表示错误源于第三方服务,例如 CDN 服务故障或消息投递超时。

错误编号是一个在0001到9999之间的四位数,用于进一步细化错误的类别。

错误码的目的

错误码的主要目的是:

  • 快速指示错误来源,帮助开发者迅速判断问题所在。

  • 清晰地对错误进行分类和标识。

  • 有助于团队成员快速达成对错误原因的共识。

2.2  在 DailyMart 中定义错误码

在 DailyMart 项目中,我们依据阿里巴巴的开发规范定义了一个错误码的枚举类。这个枚举类包含一系列预定义的错误码及其对应的错误信息。

public enum ErrorCode {
    OK("00000","操作已成功"),
    
    CLIENT_ERROR("A0001", "客户端错误"),
    USER_NOT_FOUND("A0010", "用户不存在"),
    USER_ALREADY_EXISTS("A0011", "用户已存在"),
    USERNAME_PASSWORD_INCORRECT("A0012", "用户名或密码错误"),
    VERIFICATION_CODE_EXPIRED("A0013", "验证码已过期"),
    BAD_CREDENTIALS_EXPIRED("A0014", "用户认证异常"),

    SERVICE_ERROR("B0001", "系统内部错误"),
    SERVICE_TIMEOUT_ERROR("B0010", "系统执行超时"),
    
    REMOTE_ERROR("C0001", "第三方服务错误");

    /**
     * 错误码
     */
    private final String code;
    /**
     * 错误信息
     */
    private final String message;
 ...
}

每个错误码包含两个部分:错误码和错误信息,分别由codemessage字段表示。

2.3 自定义异常的创建和使用

为了在 DailyMart 中更有效地处理错误,我们创建了三种自定义异常类:ClientException(客户端异常)、BusinessException(业务逻辑异常)和RemoteException(第三方服务异常)。这些异常类都继承自AbstractException,这是一个抽象的基类。

 

自定义异常的基类

AbstractException基类包含错误码和错误信息,同时它继承自RuntimeException,这意味着它是一个非受检异常。

@Getter
public abstract class AbstractException extends RuntimeException{

    private final String code;
    private final String message;

    public AbstractException(ErrorCode errorCode,String message,Throwable throwable){
        super(message,throwable);
        this.code = errorCode.getCode();
        this.message = Optional.ofNullable(message).orElse(errorCode.getMessage());
    }
}

定义具体的自定义异常类

接下来,我们通过继承AbstractException基类来定义具体的自定义异常类。

public class ClientException extends AbstractException{

    public ClientException(){
        this(ErrorCode.CLIENT_ERROR,null,null);
    }

    public ClientException(String message){
        this(ErrorCode.CLIENT_ERROR,message,null);
    }
 // ... 其他构造方法 ...
}

以上是ClientException的示例。我们可以为BusinessExceptionRemoteException采用类似的方式定义。

2.3 在DailyMart中实施自定义异常

现在,我们已经创建了自定义异常类,接下来让我们看看如何在 DailyMart 中使用它们来替代标准的RuntimeException

例如,在验证用户登录时,如果用户不存在,我们不再抛出普通的RuntimeException,而是抛出我们的自定义ClientException

@Override
protected CustomerUser authenticate(UserLoginDTO loginDTO) {
 CustomerUser actualUser = customerUserRepository.findByUserName(usernamePasswordLoginDTO.getUsername());
 
 if(actualUser == null){
  throw new ClientException(ErrorCode.USER_NOT_FOUND,"用户不存在");
 }
 return actualUser;
}

对于在多个地方常用的异常,我们甚至可以创建更具体的自定义异常类。例如,对于“用户不存在”的场景,我们可以创建一个UserNotFoundException类。

public class UserNotFoundException extends ClientException{

    /**
     * Constructs a <code>UsernameNotFoundException</code>
     */
    public UserNotFoundException(){
        super(ErrorCode.USER_NOT_FOUND);
    }
}

3. 全局异常处理

在处理异常时,频繁使用try...catch块可能会使代码变得混乱。为了简化异常处理并确保一致的响应格式,我们可以利用 SpringBoot 的全局异常处理功能。

3.1 使用@RestControllerAdvice进行全局异常处理

SpringBoot 提供了一个特殊的注解@RestControllerAdvice,允许我们创建全局异常处理类。在这个类中,我们可以定义处理各种类型异常的方法。

在 DailyMart 中,我们创建一个GlobalExceptionHandler类,并使用@RestControllerAdvice注解。我们主要处理三类异常:

  • MethodArgumentNotValidException:处理参数验证异常,并提供清晰的错误信息。

  • AbstractException:处理我们之前定义的自定义异常。

  • Throwable:作为最后的兜底,拦截所有其他异常。

下面是GlobalExceptionHandler的实现:

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    // 处理参数验证异常
    @SneakyThrows
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result<Void> handleValidException(HttpServletRequest request, MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
        String exceptionStr = Optional.ofNullable(firstFieldError)
                .map(FieldError::getDefaultMessage)
                .orElse(StrUtil.EMPTY);
        log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
        return ResultHelper.fail(ErrorCode.CLIENT_ERROR, exceptionStr);
    }
    
    // 处理自定义异常
    @ExceptionHandler(value = {AbstractException.class})
    public Result<Void> handleAbstractException(HttpServletRequest request, AbstractException ex) {
        String requestURL = getUrl(request);
        log.error("[{}] {} [ex] {}", request.getMethod(), requestURL, ex.toString());
        return ResultHelper.fail(ex);
    }
    
    // 兜底处理
    @ExceptionHandler(value = Throwable.class)
    public Result<Void> handleThrowable(HttpServletRequest request, Throwable throwable) {
        log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
        return ResultHelper.fail();
    }
  }
}

在启用全局异常处理功能后,DailyMart的用户模块不再需要在接口层手动使用try...catch来处理异常。倘若出现其他异常,它们也会被defaultErrorHandler拦截,从而确保DailyMart能够一致地实施统一的返回格式。

经优化后,接口层代码变得更为简洁:

@PostMapping("/api/customer/register")  
public Result<UserRegistrationDTO> register(@RequestBody @Valid UserRegistrationDTO customerDTO){  
 return ResultHelper.success(customerService.register(customerDTO));  
}

@PostMapping("/api/customer/login")
public Result<UserLoginRespDTO> login(@RequestBody Map<String, String> parameters){
 UserLoginDTO loginDTO = LoginDTOFactory.getLoginDTO(parameters);
 return ResultHelper.success(customerService.login(loginDTO));
}

4. 自动包装类

注意到目前所有的接口都需要通过手动调用 ResultHelper.success() 来对结果进行包装。这些重复的代码段可以优化吗?

答案是肯定的。在SpringBoot中,我们可以利用 ResponseBodyAdvice 来自动包装响应体。

提示ResponseBodyAdvice 可以拦截控制器(Controller)方法的返回值,允许我们统一处理返回值或响应体。这对于统一返回格式、加密、签名等场景非常有用。

在 DailyMart 中,我们可以创建一个实现 ResponseBodyAdvice 接口的类,来自动包装响应体。下面是示例代码:

@Slf4j
@RestControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;

    // 此处可以通过判断决定哪些响应需要包装
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if(body instanceof String){
            // 当响应体是String类型时,使用ObjectMapper转换,因为Spring默认使用StringHttpMessageConverter处理字符串,不会将字符串识别为JSON
            return objectMapper.writeValueAsString(ResultHelper.success(body));
        }
        if(body instanceof Result<?>){
            // 已经包装过的结果无需再次包装
            return body;
        }
        // 对响应体进行包装
        return ResultHelper.success(body);
    }
}

经过这样的优化,我们的控制器层代码可以直接简写如下:

@PostMapping("/api/customer/register")
public UserRegistrationDTO register(@RequestBody @Valid UserRegistrationDTO customerDTO){
 return customerService.register(customerDTO);
}

@PostMapping("/api/customer/login")
public UserLoginRespDTO login(@RequestBody Map<String, String> parameters){
 UserLoginDTO loginDTO = LoginDTOFactory.getLoginDTO(parameters);
 return customerService.login(loginDTO);
}

5. 定义starter

考虑到 DailyMart 项目包含多个服务,并且在其他服务中也需要全局异常处理和响应体自动包装的功能,我们可以将这些功能封装成一个 Spring Boot Starter。这样,任何需要这些功能的模块只需引入该 Starter 即可。

@SpringBootConfiguration
@ConditionalOnWebApplication
public class WebAutoConfiguration {

    /**
     * 自定义全局异常处理器
     */
    @Bean
    @ConditionalOnMissingBean(GlobalExceptionHandler.class)
    public GlobalExceptionHandler dailyMartGlobalExceptionHandler() {
        return new GlobalExceptionHandler();
    }

    /**
     *  接口自动包装
     */
    @Bean
    @ConditionalOnMissingBean(GlobalResponseBodyAdvice.class)
    public GlobalResponseBodyAdvice dailyMartGlobalResponseBodyAdvice(){
        return new GlobalResponseBodyAdvice();
    }
}

我们还需要在 resources/META-INF/spring 目录下创建一个名为 org.springframework.boot.autoconfigure.AutoConfiguration.imports 的文件,并在此文件中声明我们的自动配置类,以便 Spring Boot 在启动时能够找到并加载它。

com.jianzh5.dailymart.springboot.starter.web.config.WebAutoConfiguration

这样,当其他服务需要使用全局异常处理和自动响应体包装时,只需在它们的 pom.xml 文件中添加对这个 Starter 的依赖即可。

6、小结

本文主要讨论了SpringBoot项目中响应体自动包装和全局异常处理的优化方法。通过使用ResponseBodyAdvice接口,我们能够自动化响应体的包装过程,消除了冗余的代码。此外,我们还探讨了如何创建一个Spring Boot Starter,以将全局异常处理和自动包装类作为插件,从而方便地在多个服务中重用这些功能。这些优化措施有助于简化代码,提高可维护性和项目效率。

DDD&微服务系列源码已经上传至GitHub,如果需要获取源码地址,请关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/693403.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能python:Python语言单词大全:从入门到精通

Python语言单词大全&#xff1a;从入门到精通 Python是一种高级编程语言&#xff0c;适用于各种应用领域&#xff0c;如数据科学、机器学习、网站开发和自动化脚本编写等。Python语言有其内置的语法和功能&#xff0c;我们将在本文中逐一介绍和解释。 1. Python中最基本的单词…

K8S集群安全之安全机制

1. 安全机制说明&#xff1a; Kubernetes作为一个分布式集群的管理工具&#xff0c;保证集群的安全性是其一个重要的任务。API Server是集群内部各个组件通信的中介&#xff0c;也是外部控制的入口。所以Kubernetes的安全机制基本就是围绕保护APIServer来设计的。Kubernetes使…

小程序实现双列布局

目录 1 双列布局2 采用流式布局3 采用网格布局总结 小程序中双列布局是一个常见的效果&#xff0c;比如在电商小程序的商品推荐&#xff0c;效果如下&#xff1a; 1 双列布局 双列布局的话特点是随着数据的增多&#xff0c;我们会出现偶数或者奇数的问题&#xff0c;如果是偶…

【Data Studio 2.1.0连接openGauss2.1.0】

Data Studio 2.1.0连接openGauss2.1.0 一、实验环境说明二、配置客户端接入方式三、Data Studio2.1.0连接openGauss数据库 一、实验环境说明 openGauss2.1.0版本Data Studio 2.1.0 版本 二、配置客户端接入方式 【以下操作是omm用户进行】 修改配置文件pg_hba.conf&#xf…

12--Gradle进阶 - Gradle任务的类型

12--Gradle进阶 - Gradle任务的类型 任务类型 前面我们定义的task 都是DefaultTask 类型的,如果要完成某些具体的操作完全需要我们自己去编写gradle 脚本&#xff0c;势必有些麻烦&#xff0c;那有没有一些现成的任务类型可以使用呢&#xff1f;有的&#xff0c;Gradle 官网给出…

C. Tenzing and Balls - dp

分析&#xff1a; 补题。当时不明白的点是如何快速查询相同元素的下标&#xff0c;可以用last[a[i]]表示与a[i]相同的最近的一个数的下标&#xff0c;可以遍历数组的同时实现下标的查询和变化&#xff0c;不断通过遍历来更新last[a[i]]的值&#xff0c;即last[a[i]]i。 然后是状…

衡石bi的几种跳转方式

衡石bi的几种跳转方式 通过点击表格的单元格跳转(带参) 点击首页的表格里任意一行的单元格跳转到明细页的企业相关数据 这两个数据集都是有一个info_uuid的主键&#xff0c;我们知道每个控件都可以设置交互的方式进行跳转应用内的仪表盘。 点击首页里要设置跳转的控件右上角的…

maven基础教程

一、安装moven 1、下载maven包 首先到maven官网下载安装包&#xff0c;解压到本地目录&#xff0c;然后配置环境变量。 maven下载地址&#xff1a;https://maven.apache.org/download.cgi 2、配置环境变量 然后打开环境变量&#xff0c;添加 MAVEN_HOME 系统变量&#xff…

JVM-jvisualvm性能监控可视化工具使用与eden-s0-s1分配分析

目录 第一步&#xff1a;安装jvisualvm 第二步&#xff1a;安装VisualvmGc插件 方式一&#xff1a;jvisualvm工具直接下载安装 方式二&#xff1a;去官网下载导入安装 总结 第三步&#xff1a;idea安装VisualvM Launcher插件 第四步&#xff1a;演示young中eden、s0、s1垃…

轻量级数据交换格式:jsoncpp

"这不属于我&#xff0c;因为沉默背后&#xff0c;也有冲动" 一、认识json (1) 为什么有那么多的数据交换格式&#xff1f; 比如说&#xff0c;现在我有下面的数据: #include <iostream> #include <string>int main() {std::string name "张三&q…

比较两个突出的node.js框架:koa和express

目录 一、Koa 和 Express 的介绍 二、Koa 和 Express 的区别 1.异步编程方式不同 2.错误处理方式不同 三、Koa 和 Express 的优缺点 1.Express 的优点 2.Express 的缺点 3.Koa 的优点 4.Koa 的缺点 总结 接上文讲述了 koa框架&#xff0c;这边文章比较一下这两个突出…

CentOS系统忘记密码了改怎么修改重置103.88.34.X

首先&#xff0c;打开centos7&#xff0c; 在正常系统入口按↑和↓"e"&#xff0c;会进入edit模式 然后找到以“Linux16”开头的行&#xff0c;在该行的最后面输入“init/bin/sh” 按下ctrlx组合键来启动系统 接下来再输入“mount -o remount,rw /”(注意mount与…

ceph对象存储使用总结

ceph对象存储使用总结 大纲 基础概念安装对象存储网关RADOS网关配置对象存储网关RADOS网关修改RADOS网关端口修改支持https访问RADOS网关使用s3cmd操作对象存储Ceph dashboard管理RADOS网关使用java程序操作对象存储 基础准备 操作系统 ubuntu~18.04ceph版本 Octopus v15.2…

适用于 3DS Max 和 Cinema 4D 的 Chaos Corona 10重磅推出!

Chaos 发布了 Corona 10&#xff0c;适用于3DS Max和Cinema 4D 的渲染器的最新版本 &#xff01;Corona 10 对软件的多项现有功能进行了更新&#xff0c;包括贴花、体积渲染、景深和程序云。 目前渲云云渲染已支持Corona 10&#xff0c;支持批量渲染&#xff0c;批量出结果&am…

chatgpt赋能python:介绍Python语言

介绍Python语言 Python是一门易学易用的编程语言&#xff0c;它被广泛应用于各种领域&#xff0c;包括数据科学、机器学习、人工智能、Web开发等。自1991年首次推出以来&#xff0c;Python不断演进和发展&#xff0c;如今已经成为行业内一种不可或缺的编程语言。它在可读性和语…

chatgpt赋能python:Python词汇量为什么很重要?

Python词汇量为什么很重要&#xff1f; 作为一个有10年Python编程经验的工程师&#xff0c;我有着非常深刻的体会&#xff0c;词汇量对于掌握这门编程语言来说是非常重要的。在这篇文章中&#xff0c;我将重点讨论Python词汇量为什么很重要&#xff0c;以及如何提升你的Python…

STM32的五个时钟源

①HSI是高速内部时钟&#xff0c;RC振荡器&#xff0c;频率为8MHz。 ②HSE是高速外部时钟&#xff0c;可接石英/陶瓷谐振器&#xff0c;或者接外部时钟源&#xff0c;频率范围为4MHz~16MHz。 ③LSI是低速内部时钟&#xff0c;RC振荡器&#xff0c;频率为40kHz。 ④LSE是低速外…

2核4G服务器_4M带宽_CPU性能测评_60G系统盘

阿里云2核4G服务器297元一年、4M公网带宽、60G系统盘&#xff0c;阿里云轻量应用服务器2核4G4M带宽配置一年297.98元&#xff0c;2核2G3M带宽轻量服务器一年108元12个月&#xff0c;如下图&#xff1a; 目录 阿里云2核4G4M轻量应用服务器 2核4G服务器限制条件 轻量服务器介…

使用Spring Boot、MyBatis Plus和Elasticsearch的简单示例

下面是一个使用Spring Boot、MyBatis Plus和Elasticsearch的简单示例&#xff1a; 首先&#xff0c;在pom.xml文件中添加以下依赖&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elast…

《Pytorch深度学习和图神经网络(卷 1)》学习笔记——第五章

全连接神经网络 隐藏层的节点数决定了模型拟合能力&#xff0c;如果在单隐藏层设置足够多的节点&#xff0c;理论上可以拟合世界上各种维度的数据进行任意规则的分类&#xff0c;但会过拟合。 隐藏层的数量决定了其泛化能力&#xff0c;层数越多&#xff0c;推理的能力越强&am…