当前位置: 首页 > article >正文

SpringBoot统一数据返回格式 统一异常处理

统一数据返回格式 & 统一异常处理

  • 1. 统一数据返回格式
    • 1.1 快速入门
    • 1.2 存在问题
    • 1.3 案列代码修改
    • 1.4 优点
  • 2. 统一异常处理

1. 统一数据返回格式

强制登录案例中,我们共做了两部分⼯作

  1. 通过Session来判断⽤⼾是否登录
  2. 对后端返回数据进⾏封装,告知前端处理的结果

回顾

后端统⼀返回结果

@Data
public class Result<T> {
    private int status;
    private String errorMessage;
    private T data;
}

后端逻辑处理

@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest) {
    log.info("获取图书列表, pageRequest:{}", pageRequest);
    //⽤⼾登录, 返回图书列表 
    PageResult<BookInfo> pageResult =
            bookService.getBookListByPage(pageRequest);
    log.info("获取图书列表222, pageRequest:{}", pageResult);
    return Result.success(pageResult);
}

Result.success(pageResult)就是对返回数据进⾏了封装

拦截器帮我们实现了第⼀个功能,接下来看SpringBoot对第⼆个功能如何⽀持

1.1 快速入门

统⼀的数据返回格式使⽤@ControllerAdvice 和ResponseBodyAdvice 的⽅式实现 @ControllerAdvice 表⽰控制器通知类

添加类 ResponseAdvice ,实现 ResponseBodyAdvice 接⼝,并在类上添加 @ControllerAdvice 注解

import com.example.demo.model.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import
        org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest
                                          request, ServerHttpResponse response) {
        return Result.success(body);
    }
}
  • supports⽅法:判断是否要执⾏beforeBodyWrite⽅法.true为执⾏,false不执⾏.通过该⽅法可以 选择哪些类或哪些⽅法的response要进⾏处理,其他的不进⾏处理.

从returnType获取类名和⽅法名

//获取执⾏的类 
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
        //获取执⾏的⽅法 
        Method method = returnType.getMethod();
  • beforeBodyWrite⽅法:对response⽅法进⾏具体操作处理

测试

测试接⼝:http://127.0.0.1:8080/book/queryBookById?bookId=1

添加统⼀数据返回格式之前:


添加统一数据返回格式之后:

1.2 存在问题

我们继续测试修改图书的接口: http://127.0.0.1:8080/book/updateBook

在这里插入图片描述

结果显示, 发生内部错误
查看数据库, 发现数据库操作成功

查看日志, 日志报错:


多测试几种不同的返回结果, 发现只有返回结果为 String类型时才有这种错误发生.
测试代码:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1(){
        return "t1";
    }
    @RequestMapping("/t2")
    public boolean t2(){
        return true;
    }
    @RequestMapping("/t3")
    public Integer t3(){
        return 200;
    }
}

解决方案:

import com.example.demo.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import
org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    private static ObjectMapper mapper = new ObjectMapper();
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest
                                          request, ServerHttpResponse response) {
        //如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化
        if (body instanceof String){
            return mapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

重新测试, 结果返回正常:

原因分析:
SpringMVC默认会注册⼀些⾃带的 HttpMessageConverter

(从先后顺序排列分别为 ByteArrayHttpMessageConverter ,StringHttpMessageConverter , SourceHttpMessageConverter , SourceHttpMessageConverter ,AllEncompassingFormHttpMessageConverter )

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
    //...
    public RequestMappingHandlerAdapter() {
        this.messageConverters = new ArrayList<>(4);
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        if (!shouldIgnoreXml) {
            try {
                this.messageConverters.add(new SourceHttpMessageConverter<>());
            }
            catch (Error err) {
                // Ignore when no TransformerFactory implementation is available
            }
        }
        this.messageConverters.add(new
                AllEncompassingFormHttpMessageConverter());
    }
    //...
}

其中AllEncompassingFormHttpMessageConverter会根据项⽬依赖情况添加对应的 HttpMessageConverter

public AllEncompassingFormHttpMessageConverter() {
    if (!shouldIgnoreXml) {
        try {
            addPartConverter(new SourceHttpMessageConverter<>());
        }
        catch (Error err) {
            // Ignore when no TransformerFactory implementation is available
        }
        if (jaxb2Present && !jackson2XmlPresent) {
            addPartConverter(new Jaxb2RootElementHttpMessageConverter());
        }
    }
    if (kotlinSerializationJsonPresent) {
        addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
    }
    if (jackson2Present) {
        addPartConverter(new MappingJackson2HttpMessageConverter());
    }
    else if (gsonPresent) {
        addPartConverter(new GsonHttpMessageConverter());
    }
    else if (jsonbPresent) {
        addPartConverter(new JsonbHttpMessageConverter());
    }
    if (jackson2XmlPresent && !shouldIgnoreXml) {
        addPartConverter(new MappingJackson2XmlHttpMessageConverter());
    }
    if (jackson2SmilePresent) {
        addPartConverter(new MappingJackson2SmileHttpMessageConverter());
    }
}

在依赖中引⼊jackson包后,容器会把 MappingJackson2HttpMessageConverter ⾃动注册到 messageConverters 链的末尾.

Spring会根据返回的数据类型,从 messageConverters 链选择合适的 HttpMessageConverter .

当返回的数据是⾮字符串时,使⽤的 MappingJackson2HttpMessageConverter 写⼊返回对象. 当返回的数据是字符串时, StringHttpMessageConverter 会先被遍历到,这时会认为 StringHttpMessageConverter 可以使⽤.

public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver 
        implements HandlerMethodReturnValueHandler {

    //...代码省略
    protected <T> void writeWithMessageConverters(@Nullable T value,
                                                  MethodParameter returnType,
                                                  ServletServerHttpRequest inputMessage, ServletServerHttpResponse
                                                          outputMessage)
            throws IOException, HttpMediaTypeNotAcceptableException,
            HttpMessageNotWritableException {
        //...代码省略
        if (selectedMediaType != null) {
            selectedMediaType = selectedMediaType.removeQualityValue();
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
                        (GenericHttpMessageConverter<?>) converter : null);
                if (genericConverter != null ?
                        ((GenericHttpMessageConverter)
                                converter).canWrite(targetType, valueType, selectedMediaType) :
                        converter.canWrite(valueType, selectedMediaType)) {
                    //getAdvice().beforeBodyWrite 执⾏之后, body转换成了Result类型的结果
                            body = getAdvice().beforeBodyWrite(body, returnType,
                            selectedMediaType,
                            (Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage);
                    if (body != null) {
                        Object theBody = body;
                        LogFormatUtils.traceDebug(logger, traceOn ->
                                "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
                        addContentDispositionHeader(inputMessage, outputMessage);
                        if (genericConverter != null) {
                            genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                        }
                        else {
                            //此时cover为StringHttpMessageConverter 
                            ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                        }
                    }
                    else {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Nothing to write: null body");
                        }
                    }
                    return;
                }
            }
        }
        //...代码省略 

    }
    //...代码省略 
}

在 ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage) 的处理中,调⽤⽗类的write⽅法

由于 StringHttpMessageConverter 重写了addDefaultHeaders⽅法,所以会执⾏⼦类的⽅法


然⽽⼦类 StringHttpMessageConverter 的addDefaultHeaders⽅法定义接收参数为String,此 时t为Result类型,所以出现类型不匹配"Resultcannotbecasttojava.lang.String"的异常

1.3 案列代码修改

如果⼀些⽅法返回的结果已经是Result类型了,那就直接返回Result类型的结果即可

@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
                              MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest
                                      request, ServerHttpResponse response) {
    //返回结果更加灵活 
    if (body instanceof Result){
        return body;
    }
    //如果返回结果为String类型, 使⽤SpringBoot内置提供的Jackson来实现信息的序列化 
    if (body instanceof String){
        return mapper.writeValueAsString(Result.success(body));
    }
    return Result.success(body);
}

1.4 优点

  1. ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就可以了,因为所有接⼝都是这样返回 的.
  3. 有利于项⽬统⼀数据的维护和修改.
  4. 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容.

2. 统一异常处理

统⼀异常处理使⽤的是@ControllerAdvice +@ExceptionHandler 来实现的, @ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表 ⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件.

具体代码如下:

import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
@ResponseBody
public class ErrorAdvice {
    @ExceptionHandler
    public Object handler(Exception e) {
        return Result.fail(e.getMessage());
    }
}

类名,⽅法名和返回值可以⾃定义,重要的是注解

接⼝返回为数据时,需要加 @ResponseBody 注解

以上代码表⽰,如果代码出现Exception异常(包括Exception的⼦类),就返回⼀个Result的对象,Result 对象的设置参考Result.fail(e.getMessage())

public static Result fail(String msg) {
    Result result = new Result();
    result.setStatus(ResultStatus.FAIL);
    result.setErrorMessage(msg);
    result.setData("");
    return result;
}

我们可以针对不同的异常,返回不同的结果.

import com.example.demo.model.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {
    @ExceptionHandler
    public Object handler(Exception e) {
        return Result.fail(e.getMessage());
    }
    @ExceptionHandler
    public Object handler(NullPointerException e) {
        return Result.fail("发⽣NullPointerException:"+e.getMessage());
    }
    @ExceptionHandler
    public Object handler(ArithmeticException e) {
        return Result.fail("发⽣ArithmeticException:"+e.getMessage());
    }
}

模拟制造异常:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {
    @RequestMapping("/t1")
    public String t1(){
        return "t1";
    }
    @RequestMapping("/t2")
    public boolean t2(){
        int a = 10/0; //抛出ArithmeticException
        return true;
    }
    @RequestMapping("/t3")
    public Integer t3(){
        String a =null;
        System.out.println(a.length()); //抛出NullPointerException
        return 200;
    }
}

当有多个异常通知时,匹配顺序为当前类及其⼦类向上依次匹配

/test/t2抛出ArithmeticException,运⾏结果如下:

/test/t3抛出NullPointerException,运⾏结果如下:


http://www.kler.cn/a/518615.html

相关文章:

  • aws(学习笔记第二十六课) 使用AWS Elastic Beanstalk
  • 使用python-docx包进行多文件word文字、字符批量替换
  • Flink把kafa数据写入Doris的N种方法及对比。
  • 在Ubuntu上使用Apache+MariaDB安装部署Nextcloud并修改默认存储路径
  • 奇怪的单词(快速扩张200个单词)
  • docker 简要笔记
  • 填坑 hydra 暴力破解
  • 【开源免费】基于Vue和SpringBoot的常规应急物资管理系统(附论文)
  • pytest自动化测试 - 构造“预置条件”的几种方式
  • RAG如何让生成AI更智能?最新方法与优劣深度解析
  • 【linux】linux c判断IP地址类型及是否合法
  • Spring MVC中HandlerInterceptor的作用及应用场景
  • CVE-2025-0411 7-zip 漏洞复现
  • 防抖与节流:优化高频事件的两种利器
  • 「pandas」python pandas 初步、数据结构Series、DataFrame、MultiIndex
  • 【最小堆】【动态规划】力扣264. 丑数 II
  • 【Elasticsearch】eland是啥?
  • F#语言的图形用户界面
  • MarsCode青训营打卡Day11(2025年1月24日)|稀土掘金-373.字母出现次数的统计
  • 【OpenGL】OpenGL游戏案例(一)
  • Leetcode-两数之和
  • 【第六天】零基础入门刷题Python-算法篇-数据结构与算法的介绍-一种常见的贪心算法(持续更新)
  • flutter跨端UI框架简介
  • 简识JVM中的STW
  • 【二叉树】4. 判断一颗二叉树是否是平衡二叉树。5. 对称二叉树。6. 二叉树的构建及遍历 7. 二叉树的分层遍历 。
  • 使用 .NET Core 6.0 Web API 上传单个和多个文件