Hello, I am Misty.

Today we are going to talk about how to return to a uniform standard format in a friendly manner and how to handle global exceptions gracefully in springBoot-based front-end separation development mode.

First let’s see why we want to return a uniform standard format.

Why return a uniform standard format for SpringBoot

By default, there are three common return formats for SpringBoot:

Type 1: Mandatory String

@GetMapping("/hello")
public String getStr(a){
  return "hello,javadaily";
}
Copy the code

The return value from the call interface looks like this:

hello,javadaily
Copy the code

Second: return a custom object

@GetMapping("/aniaml")
public Aniaml getAniaml(a){
  Aniaml aniaml = new Aniaml(1."pig");
  return aniaml;
}
Copy the code

The return value from the call interface looks like this:

{
  "id": 1."name": "pig"
}
Copy the code

Type 3: The interface is abnormal

@GetMapping("/error")
public int error(){
    int i = 9/0;
    return i;
}
Copy the code

The return value from the call interface looks like this:

{
  "timestamp": "The 2021-07-08 T08:05:15. 423 + 00:00"."status": 500."error": "Internal Server Error"."path": "/wrong"
}
Copy the code

Because of all of the above, if you interface with the front-end developers, they are confused, because we don’t give them a unified format and they don’t know what to do with the return values.

Moreover, some students like To encapsulate the results, such as Xiao Zhang, who uses the Result object. Xiao Wang also likes to encapsulate the results, but he uses the Response object. When this happens, I believe the front-end staff will be crazy.

Therefore, we need to define a unified standard return format in our project.

Definition returns the standard format

A standard return format contains at least three parts:

  1. Status Status value: The status code of each returned result is defined by the backend
  2. Message description: Description of the result of this interface call
  3. Data: Indicates the returned data.
{
  "status":"100"."message":"Operation successful"."data":"hello,javadaily"
}
Copy the code

Additional extension values can be added as needed, such as the interface call time in the return object

  1. Timestamp: indicates the invocation time of the interface

Defining return objects

@Data
public class ResultData<T> {
  /** Result status. For details, see resultData.java */
  private int status;
  private String message;
  private T data;
  private long timestamp ;


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


  public static <T> ResultData<T> success(T data) {
    ResultData<T> resultData = new ResultData<>();
    resultData.setStatus(ReturnCode.RC100.getCode());
    resultData.setMessage(ReturnCode.RC100.getMessage());
    resultData.setData(data);
    return resultData;
  }

  public static <T> ResultData<T> fail(int code, String message) {
    ResultData<T> resultData = new ResultData<>();
    resultData.setStatus(code);
    resultData.setMessage(message);
    returnresultData; }}Copy the code

Define status code

Public enum ReturnCode {/** Operation succeeded **/ RC100(100," operation succeeded "), /** operation failed **/ RC999(999," operation failed "), /** service traffic limiting **/ RC200(200," Service traffic limiting is enabled, please try again later!" ), /** service degraded **/ RC201(201," Service degraded protection enabled, please try again later!" ), /** Hotspot parameter limit **/ RC202(202," Hotspot parameter limit, please try again later!" ), /** system rule does not meet **/ RC203(203," system rule does not meet requirements, please try again later! ), /** Authorization rule failed **/ RC204(204," Authorization rule failed, please try again later!" ), /**access_denied**/ RC403(403," no access permission, please contact administrator to grant access permission "), /**access_denied**/ RC401(401," anonymous access error "), RC500(500," system exception, Please try again later "), INVALID_TOKEN(2001," Access token invalid "), ACCESS_DENIED(2003," No permission to access this resource "), CLIENT_AUTHENTICATION_FAILED(1001," client authentication failed "), USERNAME_OR_PASSWORD_ERROR(1002," user name or password error "), UNSUPPORTED_GRANT_TYPE(1003, "unsupported authentication mode "); /** Private final int code; /** Custom description **/ private final String message; ReturnCode(int code, String message){ this.code = code; this.message = message; } public int getCode() { return code; } public String getMessage() { return message; }}Copy the code

Uniform return format

@GetMapping("/hello")
public ResultData<String> getStr(a){
	return ResultData.success("hello,javadaily");
}
Copy the code

The return value from the call interface looks like this:

{
  "status": 100."message": "hello,javadaily"."data": null."timestamp": 1625736481648."httpStatus": 0
}
Copy the code

This does achieve the desired result, and I’ve seen it written this way in many projects, wrapping the returned result in resultData.success () at the Controller layer and returning it to the front end.

So let’s stop and think, what’s the downside of this?

The biggest drawback is that we need to call every interface we writeResultData.success()This line of code wraps up the result, reinventing the wheel, wasting your energy; And it’s easy to be laughed at by other older birds.

So we need to optimize our code so that we don’t manually specify the ResultData return value for each interface.

Advanced implementation

Optimizing this code is as simple as taking the ResponseBodyAdvice provided by SpringBoot.

ResponseBodyAdvice intercepts the return value of the Controller method and processes the return value/response body uniformly.

ResponseBodyAdvice

public interface ResponseBodyAdvice<T> {
		/** * Whether advice is supported * true supported, false not supported */
    boolean supports(MethodParameter var1, Class
       > var2);

	  /** * Process the returned data */
    @Nullable
    T beforeBodyWrite(@NullableT var1, MethodParameter var2, MediaType var3, Class<? extends HttpMessageConverter<? >> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}
Copy the code

We just need to write a concrete implementation class

/ * * *@author jam
 * @date 2021/7/8 10:10 上午
 */
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter methodParameter, Class
       > aClass) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class
       > aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if(o instanceof String){
            return objectMapper.writeValueAsString(ResultData.success(o));
        }    
        returnResultData.success(o); }}Copy the code

There are two things to note:

  • @ RestControllerAdvice annotations

    @RestControllerAdvice is an enhancement to the @RestController annotation that does three things:

    1. Global exception handling
    2. Global data binding
    3. Global data preprocessing
  • String judgment

if(o instanceof String){
  return objectMapper.writeValueAsString(ResultData.success(o));
} 
Copy the code

This code must be added, if the Controller returns String, SpringBoot returns String, so we need to manually convert to JSON.

After the above processing, we no longer need to use resultData.success () to convert, directly return the original data format, SpringBoot automatically help us to achieve the wrapper class encapsulation.

@GetMapping("/hello")
public String getStr(a){
    return "hello,javadaily";
}
Copy the code

At this point, we call the interface and return the data as follows:

{
  "status": 100."message": "Operation successful"."data": "hello,javadaily"."timestamp": 1626427373113
}
Copy the code

Doesn’t it feel perfect? Don’t worry, there’s a problem waiting for you.

Abnormal interface

There is a problem here, because we are not handling Controller exceptions, we will have problems when we call methods with exceptions, such as the following interface

@GetMapping("/wrong")
public int error(a){
    int i = 9/0;
    return i;
}
Copy the code

The result returned is:

This is obviously not the result we want, the interface error return operation success response code, the front end will hit people.

Now let’s move on to the second topic, how to handle global exceptions gracefully.

Why does SpringBoot need a global exception handler

  1. Don’t write try… Catch, which is uniformly caught by the global exception handler

    One of the biggest benefits of using a global exception handler is that programmers no longer need to write a try by hand… By default, SpringBoot returns the following result when an exception occurs:

{
  "timestamp": "The 2021-07-08 T08:05:15. 423 + 00:00"."status": 500."error": "Internal Server Error"."path": "/wrong"
}
Copy the code
This data format is returned to the front end, which can't read it, so we usually pass itCopy the code

try… Catch to handle exceptions

@GetMapping("/wrong")
public int error(a){
    int i;
    try{
        i = 9/0;
    }catch (Exception e){
        log.error("error:{}",e);
        i = 0;
    }
    return i;
}
Copy the code

The goal is definitely not to have to write try… Catch, and instead expect it to be handled by the global exception handler.

  1. Custom exceptions can only be handled by a global exception handler
@GetMapping("error1")
public void empty(a){
	throw  new RuntimeException("Custom exception");
}
Copy the code
  1. When we introduce a Validator, an exception will be thrown if the Validator fails. If caught, only global exception handlers can be used.

    SpringBoot integration parameter verification please refer to this article SpringBoot development secrets – integration parameter verification and advanced skills

How to implement a global exception handler

@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    /** * Default global exception handling. *@param e the e
     * @return ResultData
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultData<String> exception(Exception e) {
        log.error("Global exception information ex={}", e.getMessage(), e);
        returnResultData.fail(ReturnCode.RC500.getCode(),e.getMessage()); }}Copy the code

There are three details to note:

  1. @RestControllerAdvice, an enhanced class for RestController that can be used to implement global exception handlers
  2. @ExceptionHandler, unified handling of a certain type of exception, thus reducing code duplication and complexity, such as to obtain a custom exception can@ExceptionHandler(BusinessException.class)
  3. @ResponseStatusSpecifies the HTTP status code received by the client

Experience the effect

At this point we call the following interface:

@GetMapping("error1")
public void empty(a){
    throw  new RuntimeException("Custom exception");
}
Copy the code

The result is as follows:

{
  "status": 500."message": "Custom exception"."data": null."timestamp": 1625795902556
}
Copy the code

That’s basically what we need.

But a new problem arises when we enable both the ResponseAdvice and RestExceptionHandler global exception handlers:

{
  "status": 100."message": "Operation successful"."data": {
    "status": 500."message": "Custom exception"."data": null."timestamp": 1625796167986
  },
  "timestamp": 1625796168008
}
Copy the code

In this case, the return result is like this. The unified format enhancement will rewrap the returned exception result, so we need to fix this problem.

Standard format for global exception access returns

Getting global exception access to the standard format is easy because the global exception handler has already wrapped the standard format for us and we just need to return it directly to the client.

@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class
       > aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
  if(o instanceof String){
    return objectMapper.writeValueAsString(ResultData.success(o));
  }
  if(o instanceof ResultData){
    return o;
  }
  return ResultData.success(o);
}
Copy the code

Key code:

if(o instanceof ResultData){
  return o;
}
Copy the code

If the return result is a ResultData object, simply return it.

At this point we call the above error method, the return result will meet our requirements.

{
  "status": 500."message": "Custom exception"."data": null."timestamp": 1625796580778
}
Copy the code

Well, that’s the end of today’s article. Hopefully, through this article you can understand how to implement uniform formatting to return in your project and handle global exceptions gracefully.

Github address: github.com/jianzh5/clo…

Finally, I am Misty Jam, an architect who writes code and a programmer who does architecture. I look forward to your attention. See you next time!

Attention is to send 10 G of teaching video, still don’t hurry to get on the bus?