SpringBoot Unified Response Body Solution

Posted by EternalSorrow on Thu, 18 Jul 2019 04:26:01 +0200

Preface

Recently, before optimizing itself, the implementation scheme of unified responder based on Spring AOP is proposed.

What is a unified responder? In the current front-end and back-end separation architecture, the back-end is mainly a RESTful API data interface.

However, the number of HTTP status codes is limited, and with the growth of business, HTTP status codes can not well represent the abnormal situation encountered in business.

You can then modify the JSON data returned by the response to bring some inherent fields, such as the following

{
    "code": 10000,
    "msg": "success",
    "data": {
        "id": 2,
        "name": "test"
    }
}

The use of key attributes is as follows:

  • Code is the status code that returns the result
  • msg is the message that returns the result
  • Data is the business data returned

These three attributes are inherent attributes, and each response result will have them.

demand

Hope to achieve a solution that can replace AOP, we need to meet the following points:

  1. The original implementation scheme based on AOP requires Controller's return type to be Object, and new scheme to be unrestricted return type.
  2. The original implementation scheme based on AOP needs to control the tangent point through the tangent expression + annotation Controller (the modification of the package name of the annotation will lead to the modification of the tangent expression, i.e. two places need to be modified). It needs a new scheme that can be based on the annotation without modifying the tangent expression.

Ideas of scheme

Based on the above requirements, Spring's Controller enhancement mechanism is chosen, and the key classes are the following three:

  • @ Controller Advice: A class annotation that specifies the Controller enhancement processor class.
  • ResponseBodyAdvice: Interface. After the implementation of beforeBodyWrite() method, the body of the response can be modified. It needs to be used in conjunction with @Controller Advice.
  • @ ExceptionHandler: Method annotations for specifying exception handling methods need to be used in conjunction with @Controller Advice and @ResponseBody.

Sample key code

The Spring Book version used in this example is 2.1.6.RELEASE, and the lombok plug-in needs to be installed by the development tool.

Introducing dependencies

    <dependencies>
        <!--web-starter-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--test-starter-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

Unified Responsor

Objects corresponding to unified response volume after Controller enhancement

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

/**
 * Unified public responder
 * @author NULL
 * @date 2019-07-16
 */
@Data
@AllArgsConstructor
public class ResponseResult implements Serializable {
    /**
     * Return status code
     */
    private Integer code;
    /**
     * Return information
     */
    private String msg;
    /**
     * data
     */
    private Object data;

}

Unified Response Annotation

Unified Response Annotations are annotations that mark whether Unified Response Enhancement is turned on or not.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Unified Response Note <br/>
 * The Unified Responsor will not take effect until annotations are added.
 * @author NULL
 * @date 2019-07-16
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface BaseResponse {

}

Enumeration of status codes

Enumeration classes corresponding to status code and status information msg returned from unified response body

/**
 * Return status code
 *
 * @author NULL
 * @date 2019-07-16
 */
public enum ResponseCode {
    /**
     * Successful return status code
     */
    SUCCESS(10000, "success"),
    /**
     * State codes for non-existent resources
     */
    RESOURCES_NOT_EXIST(10001, "Resources do not exist"),
    /**
     * Default return status codes for all unrecognized exceptions
     */
    SERVICE_ERROR(50000, "Server exception");
    /**
     * Status code
     */
    private int code;
    /**
     * Return information
     */
    private String msg;

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

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

Business exception class

Business exception classes are used to identify business-related exceptions. It is important to note that this exception class enforces the need for ResponseCode as a constructor, so that the returned status code information can be obtained by capturing exceptions.

import com.rjh.web.response.ResponseCode;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * Business exception class, inherit runtime exception, ensure normal transaction rollback
 *
 * @author NULL
 * @since  2019-07-16
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class BaseException extends RuntimeException{

    private ResponseCode code;

    public BaseException(ResponseCode code) {
        this.code = code;
    }

    public BaseException(Throwable cause, ResponseCode code) {
        super(cause);
        this.code = code;
    }
}

Exception handling class

Processing class for handling exceptions not captured at Controller runtime.

import com.rjh.web.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * Exception handler
 *
 * @author NULL
 * @since  2019-07-16
 */
@ControllerAdvice(annotations = BaseResponse.class)
@ResponseBody
@Slf4j
public class ExceptionHandlerAdvice {
    /**
     * Handling Uncaptured Exception
     * @param e abnormal
     * @return Unified Responsor
     */
    @ExceptionHandler(Exception.class)
    public ResponseResult handleException(Exception e){
        log.error(e.getMessage(),e);
        return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null);
    }

    /**
     * Handling Uncaptured Runtime Exception
     * @param e Runtime exception
     * @return Unified Responsor
     */
    @ExceptionHandler(RuntimeException.class)
    public ResponseResult handleRuntimeException(RuntimeException e){
        log.error(e.getMessage(),e);
        return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null);
    }

    /**
     * Handling Business Exception
     * @param e Business exceptions
     * @return Unified Responsor
     */
    @ExceptionHandler(BaseException.class)
    public ResponseResult handleBaseException(BaseException e){
        log.error(e.getMessage(),e);
        ResponseCode code=e.getCode();
        return new ResponseResult(code.getCode(),code.getMsg(),null);
    }
}

Response Enhancement Class

Conrtoller enhanced unified responder processing class, need to pay attention to exception handling class has been enhanced, so we need to determine whether the returned object is a unified responder object.

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;

/**
 * Unified Responsor Processor
 * @author NULL
 * @date 2019-07-16
 */
@ControllerAdvice(annotations = BaseResponse.class)
@Slf4j
public class ResponseResultHandlerAdvice implements ResponseBodyAdvice {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        log.info("returnType:"+returnType);
        log.info("converterType:"+converterType);
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if(MediaType.APPLICATION_JSON.equals(selectedContentType) || MediaType.APPLICATION_JSON_UTF8.equals(selectedContentType)){ // Judging that the Content-Type of the response is body in JSON format
            if(body instanceof ResponseResult){ // If the object returned by the response is a unified responder, the body is returned directly.
                return body;
            }else{
                // Only the normal returned result will enter the judgment process, so the normal successful status code will be returned.
                ResponseResult responseResult =new ResponseResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),body);
                return responseResult;
            }
        }
        // Non-JSON format body can be returned directly
        return body;
    }
}

Use examples

First prepare a User object

import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;

/**
 * User class
 * @author NULL
 * @date 2019-07-16
 */
@Data
@EqualsAndHashCode
public class User implements Serializable {

    private Integer id;

    private String name;
    
}

Then prepare a simple User Controller.

import com.rjh.web.entity.User;
import com.rjh.web.exception.BaseException;
import com.rjh.web.response.BaseResponse;
import com.rjh.web.response.ResponseCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Controller for testing
 *
 * @author NULL
 * @date 2019-07-16
 */
@BaseResponse
@RestController
@RequestMapping("users")
public class UserController {

    @GetMapping("/{userId}")
    public User getUserById(@PathVariable Integer userId){
        if(userId.equals(0)){
            throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
        }
        if(userId.equals(1)){
            throw new RuntimeException();
        }
        User user=new User();
        user.setId(userId);
        user.setName("test");
        return user;
    }
    
}

Operation results

  1. When the browser accesses http://127.0.0.1:8080/users/0 directly, the results are as follows (the results are formatted):

    {
        "code": 10001,
        "msg": "Resources do not exist",
        "data": null
    }
  2. When the browser directly accesses http://127.0.0.1:8080/users/1, the results are as follows (the results are formatted):

    {
        "code": 50000,
        "msg": "Server exception",
        "data": null
    }
  3. When the browser directly accesses http://127.0.0.1:8080/users/2, the results are as follows (the results are formatted):

    {
        "code": 10000,
        "msg": "success",
        "data": {
            "id": 2,
            "name": "test"
        }
    }

From the results of operation, we can know that the unified response enhancement has actually taken effect, and can handle exceptions well.

Sample code address

Here is the code address of this example. If you think it's good or helpful, I hope you can give me a Star:
https://github.com/spring-bas...

Reference material

  1. https://docs.spring.io/spring...
  2. https://docs.spring.io/spring...

Topics: Java Lombok Spring JSON