Hello, good morning, I’m the little black man downstairs

Recently, when I happened to see the official Documentation of Spring, I learned a new annotation @ControllerAdvice, and successfully used this annotation to refactor the external API interface of our project, remove tedious and repetitive code, and make its development more elegant.

Before we show you how to refactor the code, let’s take a look at how the original external API was developed.

This API interface is mainly used to interact with our APP. During this process, we uniformly define an interaction protocol, and both the APP side and the background API interface use JSON format.

In addition, when the background API interface returns to the APP, some error codes are unified. The APP side needs to pop up some prompts based on the corresponding error codes.

The following shows the interface data returned by querying user information:

{
    "code": "000000"."msg": "success"."result": {
        "id": "1"."name": "test"}}Copy the code

Code represents the external error code, MSG represents the error message, and result represents the specific returned message.

The front-end APP obtains the returned information, and first determines whether the code returned by the interface is 000000. If yes, the query is successful, and then obtains the result information and displays it accordingly. Otherwise, an error message is displayed.

Welcome to pay attention to my public number: procedure, get daily dry goods push. If you are interested in my topics, you can also follow my blog: studyidea.cn

refactoring

Now let’s look at how the backend API layer is encoded, before the refactoring.

/** * Version V1 **@return* /
@RequestMapping("testv1")
public APIResult testv1(a) {
    try {
        User user = new User();
        user.setId("1");
        user.setName("test");
        return APIResult.success(user);
    } catch (APPException e) {
        log.error("Internal anomaly", e);
        return APIResult.error(e.getCode(), e.getMsg());
    } catch (Exception e) {
        log.error("System exception", e);
        returnAPIResult.error(RetCodeEnum.FAILED); }}Copy the code

The above code is simple. It encapsulates a utility class, APIResult, internally and wraps the specific results in it.

@Data
public class APIResult<T> implements Serializable {

    private static final long serialVersionUID = 4747774542107711845L;

    private String code;

    private String msg;

    private T result;


    public static <T> APIResult success(T result) {
        APIResult apiResult = new APIResult();
        apiResult.setResult(result);
        apiResult.setCode("000000");
        apiResult.setMsg("success");
        return apiResult;
    }

    public static APIResult error(String code, String msg) {
        APIResult apiResult = new APIResult();
        apiResult.setCode(code);
        apiResult.setMsg(msg);
        return apiResult;
    }

    public static APIResult error(RetCodeEnum codeEnum) {
        APIResult apiResult = new APIResult();
        apiResult.setCode(codeEnum.getCode());
        apiResult.setMsg(codeEnum.getMsg());
        return apiResult;
    }
Copy the code

In addition to this, an exception object, APPException, is defined to wrap the various internal exceptions uniformly.

The above code is simple, but it can be quite tedious and repetitive. Every interface needs to use a try… Catch wrap, and then use APIResult to include normal return and error messages.

Second, the interface object can only return the APIResult, and the real business object can only be hidden in the APIResult. It’s not very elegant, and it’s not very intuitive to know the real business object.

After the reconstruction

Let’s refactor the above code to get rid of the duplicate try… Catch the code.

For this refactoring we need to annotate @controllerAdvice and ResponseBodyAdvice with Spring. Let’s take a look at the refactoring code first.

Ps: ResponseBodyAdvice comes from the Spring 4.2 API. If you want to use this, you may need to update the Spring version.

Overwrite return information

First we need to implement ResponseBodyAdvice, implementing our own handler class.

@ControllerAdvice
public class CustomResponseAdvice implements ResponseBodyAdvice {
    /** * Whether to process the returned result *@param methodParameter
     * @param aClass
     * @return* /
    @Override
    public boolean supports(MethodParameter methodParameter, Class aClass) {
        System.out.println("In supports() method of " + getClass().getSimpleName());
        return true;
    }

    /** * Processing returns result *@param body
     * @param methodParameter
     * @param mediaType
     * @param aClass
     * @param serverHttpRequest
     * @param serverHttpResponse
     * @return* /
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        System.out.println("In beforeBodyWrite() method of " + getClass().getSimpleName());
        if (body instanceof APIResult) {
            return body;
        }
        returnAPIResult.success(body); }}Copy the code

By implementing the above interface, we can modify the return result in the beforeBodyWrite method.

In the above code, we simply wrap the returned result in APIResult and return it. In fact, we can add some extra logic here. For example, if the interface returns information to be encrypted, we can unify the encryption at this layer.

If the body is APIResult class, return it without modification.

This is compatible with the old interface, because by default, the CustomResponseAdvice class, which we implemented ourselves, will apply to all controllers.

If you do not do this, you will have two layers of APIResul wrapped around the old return, affecting APP parsing.

In addition, if you are concerned about the impact of this change on the old interface, you can use the following method to only apply to the specified method.

First define a custom annotation, such as:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomResponse {
}
Copy the code

We annotate this in the method that needs to be changed, and then we check in ResponseBodyAdvice#supports if there is a CustomResponse annotation on the method. If there is, we return true, which means that the return class will be modified at the end. If not, return false, and the process will be the same as before.

/** whether to process the returned result **@param methodParameter
 * @param aClass
 * @return* /
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
    System.out.println("In supports() method of " + getClass().getSimpleName());
    Method method = methodParameter.getMethod();
    return method.isAnnotationPresent(CustomResponse.class);
}
Copy the code

Global exception handling

After the above code refactoring, the duplicate code is extracted and the overall code is left with our business logic, which is very neat and elegant.

However, there are still problems with the refactored code above, mainly exception handling.

If the above business code throws an exception, the interface will return a stack error message, not the one we defined. So let’s do this, let’s optimize it again.

This time we mainly need to use the @ExceptionHandler annotation, which needs to be used with @ControllerAdvice.

@Slf4j
@ControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public APIResult handleException(Exception e) {
        log.error("System exception", e);
        return APIResult.error(RetCodeEnum.FAILED);
    }

    @ExceptionHandler(APPException.class)
    @ResponseBody
    public APIResult handleAPPException(APPException e) {
        log.error("Internal anomaly", e);
        returnAPIResult.error(e.getCode(), e.getMsg()); }}Copy the code

With this @ExceptionHandler, the corresponding exception will be intercepted and then the corresponding method will be called to handle the exception. In this case, we return some error messages wrapped in APIResult.

conclusion

We can use @controllerAdvice plus ResponseBodyAdvice to intercept the returned results and make some changes. The business code you can use is simple and elegant.

In addition, we can use the @ExceptionHandler annotation for business code to perform a global exception handling that seamlessly blends with ResponseBodyAdvice.

One thing we need to do is implement the ResponseBodyAdvice class in conjunction with @ControllerAdvice. We’ll explain why this is the case in the next article when we analyze the origin. Stay tuned

Welcome to pay attention to my public number: procedure, get daily dry goods push. If you are interested in my topics, you can also follow my blog: studyidea.cn