4D analysis! Springboot's elegant unified return format + global exception handling

Table of contents

1. Custom enumeration class

public enum ReturnCode {
    RC200(200, "ok"),
    RC400(400, "请求失败,参数错误,请检查后重试。"),
    RC404(404, "未找到您请求的资源。"),
    RC405(405, "请求方式错误,请检查后重试。"),
    RC500(500, "操作失败,服务器繁忙或服务器错误,请稍后再试。");

    // 自定义状态码
    private final int code;

    // 自定义描述
    private final String msg;

    ReturnCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

This enumeration class is the return status code and description information that we agreed with the front end, and the status code and description can be modified according to your own needs

2. Customize the unified return format class

@Data
public class R<T> {

    private Integer code; //状态码

    private String msg; //提示信息

    private T data; //数据

    private long timestamp;//接口请求时间

    public R() {
        this.timestamp = System.currentTimeMillis();
    }

    public static <T> R<T> success(T data) {
        R<T> r = new R<>();
        r.setCode(ReturnCode.RC200.getCode());
        r.setMsg(ReturnCode.RC200.getMsg());
        r.setData(data);
        return r;
    }

    public static <T> R<T> error(int code, String msg) {
        R<T> r = new R<>();
        r.setCode(code);
        r.setMsg(msg);
        r.setData(null);
        return r;
    }
}

@DataAnnotations are annotations in the Lombok tool class library, which provide get, set, equals, hashCode, canEqual, and toString methods of the class. Lombok needs to be configured when using it. If not configured, please generate related methods manually.

The information we return includes at least three parts: code, msg, and data. Code is the status code agreed between our backend and frontend, msg is the prompt message, and data is the specific data returned. If there is no returned data, it is null. In addition to these three parts, you can also define some other fields, such as request time timestamp.

After the unified return class is defined, the controller layer uses uniform R.success()method encapsulation when returning data.

@RestController
@RequestMapping("/test")
public class TestController {
    @PostMapping("/test1")
    public R<List<Student>> getStudent() {
        ArrayList<Student> list = new ArrayList<>();
        Student student1 = new Student();
        student1.setId(1);
        student1.setName("name1");
        Student student2 = new Student();
        student2.setId(2);
        student2.setName("name2");
        list.add(student1);
        list.add(student2);
        return R.success(list);
    }
}

@Data
class Student {
    private Integer id;
    private String name;
}

For example, in the above code, our requirement is to query student information, and we call this test1 interface to return the following results:

{
    "code": 200,
    "msg": "ok",
    "data": [
        {
            "id": 1,
            "name": "name1"
        },
        {
            "id": 2,
            "name": "name2"
        }
    ],
    "timestamp": 1692805971309
}

image-20230823235342116

So far we have basically realized the unified return format, but the above implementation method also has a disadvantage, that is, it needs to call the method every time the data is returned, which is very R.success()troublesome. We hope to return our actual data directly in the controller layer. That is, the content in the data field is automatically encapsulated into it for us R.success(), so we need a more advanced method.

3. Advanced implementation of unified return format

We need to use the springboot ResponseBodyAdviceclass to achieve this function. The role of ResponseBodyAdvice: intercept the return value of the Controller method, and uniformly process the return value/response body

/**
 * 拦截controller返回值,封装后统一返回格式
 */
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //如果Controller返回String的话,SpringBoot不会帮我们自动封装而直接返回,因此我们需要手动转换成json。
        if (o instanceof String) {
            return objectMapper.writeValueAsString(R.success(o));
        }
        //如果返回的结果是R对象,即已经封装好的,直接返回即可。
        //如果不进行这个判断,后面进行全局异常处理时会出现错误
        if (o instanceof R) {
            return o;
        }
        return R.success(o);
    }
}

@RestControllerAdviceIt is @RestControlleran enhancement of annotations, which can realize three functions:

  1. global exception handling
  2. global data binding
  3. Global Data Preprocessing

After the above processing, we don't need to use R.success()encapsulation in the controller layer, and return the original data directly, and springboot will automatically encapsulate it for us.

@RestController
@RequestMapping("/test")
public class TestController {
    @PostMapping("/test1")
    public List<Student> getStudent() {
        ArrayList<Student> list = new ArrayList<>();
        Student student1 = new Student();
        student1.setId(1);
        student1.setName("name1");
        Student student2 = new Student();
        student2.setId(2);
        student2.setName("name2");
        list.add(student1);
        list.add(student2);
        return list;
    }
}

@Data
class Student {
    private Integer id;
    private String name;
}

At this time, the data returned by our call interface is still json data in a custom unified return format

{
    "code": 200,
    "msg": "ok",
    "data": [
        {
            "id": 1,
            "name": "name1"
        },
        {
            "id": 2,
            "name": "name2"
        }
    ],
    "timestamp": 1692805971325
}

It should be noted that even if the interface return type of our controller layer is void, ResponseBodyAdvicethe class will still automatically encapsulate it for us, and the data field is null. The returned format is as follows:

{
    "code": 200,
    "msg": "ok",
    "data": null,
    "timestamp": 1692805971336
}

4. Global exception handling

If we do not do unified exception handling, when an exception occurs in the backend, the returned data becomes as follows:

Backend interface:

@RestController
@RequestMapping("/test")
public class TestController {
    @PostMapping("/test1")
    public String getStudent() {
        int i = 1/0;
        return "hello";
    }
}

return json:

{
    "code": 200,
    "msg": "ok",
    "data": {
        "timestamp": "2023-08-23T16:13:57.818+00:00",
        "status": 500,
        "error": "Internal Server Error",
        "path": "/test/test1"
    },
    "timestamp": 1692807237832
}

The code returns 200, and a 500 error is displayed in the data. This is obviously not the result we want. The result we want should be when the code returns 500 and the data returns null. There are many ways to solve it. You can catch it by try catch, but we don't know when an exception will occur, and it is not convenient to write try catch manually. So we need global exception handling.

@Slf4j
@RestControllerAdvice
@ResponseBody
public class RestExceptionHandler {

    /**
     * 处理异常
     *
     * @param e otherException
     * @return
     */
    @ExceptionHandler(Exception.class)
    public R<String> exception(Exception e) {
        log.error("异常 exception = {}", e.getMessage(), e);
        return R.error(ReturnCode.RC500.getCode(), ReturnCode.RC500.getMsg());
    }
}

illustrate:

  1. @RestControllerAdvice, an enhanced class of RestController that can be used to implement a global exception handler
  2. @ExceptionHandler, to handle a certain type of exception uniformly, for example, to get a null pointer exception, you can@ExceptionHandler(NullPointerException.class)

In addition, you can also use @ResponseStatusto specify the http status code received by the client, for example @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR), the http status code received by the client is 500. If not specified, 200 is returned by default. We have not specified here, so the http status code returned by our request is all 200. When an exception occurs, we can modify the status code of the code in the unified return format to indicate the specific situation.

The specific effect is as follows:

interface:

@RestController
@RequestMapping("/test")
public class TestController {
    @PostMapping("/test1")
    public void test() {
        int i = 1/0; //发生除0异常
    }
}

return json:

{
    "code": 500,
    "msg": "操作失败,服务器繁忙或服务器错误,请稍后再试。",
    "data": null,
    "timestamp": 1692808061062
}

Basically fulfilled our needs.

5. More elegant global exception handling

In the global exception handling above, we caught it directly Exception.class, and handled it uniformly no matter what exception, but in fact we need to handle it differently according to different exceptions. For example, a null pointer exception may be a front-end parameter passing error, and our custom exception etc.

The custom exception is as follows:

@Getter
@Setter
public class BusinessException extends RuntimeException {
    private int code;
    private String msg;

    public BusinessException() {
    }

    public BusinessException(ReturnCode returnCode) {
        this(returnCode.getCode(),returnCode.getMsg());
    }

    public BusinessException(int code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}

Note: @Getterand @Setterprovide get and set methods respectively, which also require Lombok dependencies.

We can use specified exception types in global exception handling @ExceptionHandlerto handle different exceptions separately

@Slf4j
@RestControllerAdvice
@ResponseBody
public class RestExceptionHandler {

    /**
     * 处理自定义异常
     *
     * @param e BusinessException
     * @return
     */
    @ExceptionHandler(BusinessException.class)
    public R<String> businessException(BusinessException e) {
        log.error("业务异常 code={}, BusinessException = {}", e.getCode(), e.getMessage(), e);
        return R.error(e.getCode(), e.getMsg());
    }

    /**
     * 处理空指针的异常
     *
     * @param e NullPointerException
     * @return
     * @description 空指针异常定义为前端传参错误,返回400
     */
    @ExceptionHandler(NullPointerException.class)
    public R<String> nullPointerException(NullPointerException e) {
        log.error("空指针异常 NullPointerException ", e);
        return R.error(ReturnCode.RC400.getCode(), ReturnCode.RC400.getMsg());
    }

    /**
     * 处理其他异常
     *
     * @param e otherException
     * @return
     */
    @ExceptionHandler(Exception.class)
    public R<String> exception(Exception e) {
        log.error("未知异常 exception = {}", e.getMessage(), e);
        return R.error(ReturnCode.RC500.getCode(), ReturnCode.RC500.getMsg());
    }
}

It should be noted that an exception will only be caught once, such as a null pointer exception, which will only be caught by the second method, and will not be caught by the last method after processing. @ExceptionHandler(Exception.class)When none of the above two methods catches the specified exception, all exceptions can be caught if the last method is specified , which is equivalent to the if elseif else statement

Test custom exceptions, null pointer exceptions, and other exceptions separately:

  1. custom exception

    interface:

@RestController
@RequestMapping("/test")
public class TestController {
    @PostMapping("/test1")
    public void test() {
        throw new BusinessException(ReturnCode.RC500.getCode(),"发生异常");
    }
}

​ return json:

{
    "code": 500,
    "msg": "发生异常",
    "data": null,
    "timestamp": 1692809118244
}
  1. Null pointer exception:

    interface:

    @RestController
    @RequestMapping("/test")
    public class TestController {
        @PostMapping("/test1")
        public void test(int id, String name) {
            System.out.println(id + name);
            boolean equals = name.equals("11");
        }
    }
    

    ask:

    image-20230824005143814

    return json:

    {
        "code": 400,
        "msg": "请求失败,参数错误,请检查后重试。",
        "data": null,
        "timestamp": 1692809456917
    }
    
  2. Other exceptions:

    interface:

    @RestController
    @RequestMapping("/test")
    public class TestController {
        @PostMapping("/test1")
        public void test() {
           throw new RuntimeException("发生异常");
        }
    }
    

    return json:

    {
        "code": 500,
        "msg": "操作失败,服务器繁忙或服务器错误,请稍后再试。",
        "data": null,
        "timestamp": 1692809730234
    }
    

6. Handling 404 errors

Even if we configure global exception handling, when 4xx errors such as 404 not found occur, unexpected situations will still occur:

image-20230824010132052

return json:

{
    "code": 200,
    "msg": "ok",
    "data": {
        "timestamp": "2023-08-23T17:01:15.102+00:00",
        "status": 404,
        "error": "Not Found",
        "path": "/test/nullapi"
    },
    "timestamp": 1692810075116
}

We can see that the console does not report an exception when a 404 error occurs. The reason is that the 404 error is not an exception, and the global exception handler will naturally not catch and process it. Therefore, our solution is to let springboot report an exception directly when a 4xx error occurs, so that our global exception handling can catch it.

Add the following configuration items to application.ymlthe configuration file:

#  当HTTP状态码为4xx时直接抛出异常
spring:
  mvc:
    throw-exception-if-no-handler-found: true
  #  关闭默认的静态资源路径映射
  web:
    resources:
      add-mappings: false

Now when we request an interface that does not exist again, the console will report NoHandlerFoundExceptionan exception, which will be caught by the global exception handler and returned uniformly

return json:

{
    "code": 500,
    "msg": "操作失败,服务器繁忙或服务器错误,请稍后再试。",
    "data": null,
    "timestamp": 1692810621545
}

image-20230824011215375

NoHandlerFoundExceptionWhen a 404 error occurs, the HTTP status code is still 200, and the code returns 500, which is not conducive to the understanding of users or front-end personnel, so we can handle the exception separately in the global exception handling.

/**
     * 处理404异常
     *
     * @param e NoHandlerFoundException
     * @return
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)//指定http状态码为404
    public R<String> noHandlerFoundException(HttpServletRequest req, Exception e) {
        log.error("404异常 NoHandlerFoundException, method = {}, path = {} ", req.getMethod(), req.getServletPath(), e);
        return R.error(ReturnCode.RC404.getCode(), ReturnCode.RC404.getMsg());
    }

In the above, we use @ExceptionHandler(NoHandlerFoundException.class)separate capture to handle 404 exceptions, and at the same time use @ResponseStatus(HttpStatus.NOT_FOUND)the specified HTTP return code as 404, and the code in our unified return format is also set to 404

Now when we have a 404 exception again, the returned json is as follows:

{
    "code": 404,
    "msg": "未找到您请求的资源。",
    "data": null,
    "timestamp": 1692811047868
}

image-20230824011752721

Console log:

image-20230824012427283

Similarly, we can also configure for 405 errors, and the exceptions corresponding to 405 errors areHttpRequestMethodNotSupportedException

/**
     * 处理请求方式错误(405)异常
     *
     * @param e HttpRequestMethodNotSupportedException
     * @return
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)//指定http状态码为405
    public R<String> HttpRequestMethodNotSupportedException(HttpServletRequest req, Exception e) {
        log.error("请求方式错误(405)异常 HttpRequestMethodNotSupportedException, method = {}, path = {}", req.getMethod(), req.getServletPath(), e);
        return R.error(ReturnCode.RC405.getCode(), ReturnCode.RC405.getMsg());
    }

return json:

{
    "code": 405,
    "msg": "请求方式错误,请检查后重试。",
    "data": null,
    "timestamp": 1692811288226
}

image-20230824012201529

Console log:

image-20230824012503918

The complete code of the global exception handling RestExceptionHandlerclass is as follows:

package com.tuuli.config;

import com.tuuli.common.BusinessException;
import com.tuuli.common.R;
import com.tuuli.common.ReturnCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;

/**
 * 全局异常处理
 */
@Slf4j
@RestControllerAdvice
@ResponseBody
public class RestExceptionHandler {

    /**
     * 处理自定义异常
     *
     * @param e BusinessException
     * @return
     */
    @ExceptionHandler(BusinessException.class)
    public R<String> businessException(BusinessException e) {
        log.error("业务异常 code={}, BusinessException = {}", e.getCode(), e.getMessage(), e);
        return R.error(e.getCode(), e.getMsg());
    }

    /**
     * 处理空指针的异常
     *
     * @param e NullPointerException
     * @return
     * @description 空指针异常定义为前端传参错误,返回400
     */
    @ExceptionHandler(value = NullPointerException.class)
    public R<String> nullPointerException(NullPointerException e) {
        log.error("空指针异常 NullPointerException ", e);
        return R.error(ReturnCode.RC400.getCode(), ReturnCode.RC400.getMsg());
    }

    /**
     * 处理404异常
     *
     * @param e NoHandlerFoundException
     * @return
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public R<String> noHandlerFoundException(HttpServletRequest req, Exception e) {
        log.error("404异常 NoHandlerFoundException, method = {}, path = {} ", req.getMethod(), req.getServletPath(), e);
        return R.error(ReturnCode.RC404.getCode(), ReturnCode.RC404.getMsg());
    }

    /**
     * 处理请求方式错误(405)异常
     *
     * @param e HttpRequestMethodNotSupportedException
     * @return
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public R<String> HttpRequestMethodNotSupportedException(HttpServletRequest req, Exception e) {
        log.error("请求方式错误(405)异常 HttpRequestMethodNotSupportedException, method = {}, path = {}", req.getMethod(), req.getServletPath(), e);
        return R.error(ReturnCode.RC405.getCode(), ReturnCode.RC405.getMsg());
    }

    /**
     * 处理其他异常
     *
     * @param e otherException
     * @return
     */
    @ExceptionHandler(Exception.class)
    public R<String> exception(Exception e) {
        log.error("未知异常 exception = {}", e.getMessage(), e);
        return R.error(ReturnCode.RC500.getCode(), ReturnCode.RC500.getMsg());
    }
}

Guess you like

Origin blog.csdn.net/wdj_yyds/article/details/132473557