Refuse to be cumbersome, SpringBoot interceptor and unified function processing

Foreword

Spring AOP is a framework based on aspect-oriented programming, which is used to separate cross-cutting concerns (such as logging, transaction management) from business logic, and weave these concerns into the target object’s methods before and after execution and throw through proxy objects. Executed at specific locations such as when an exception occurs or results are returned, thereby improving the reusability, maintainability and flexibility of the program.

However, it is very cumbersome and difficult to implement unified interception using native Spring AOP. In this section, we will use a simple method to perform unified function processing, which is also a practical application of AOP, as follows:

  • Unified user login permission verification

  • Unified data format return

  • Unified exception handling

0 Why do we need unified function processing?

Unified function processing is a design idea to improve the maintainability, reusability and scalability of code. In an application, there may be some common functional requirements, such as authentication, logging, exception handling, etc.

These functions need to be called and processed in multiple places. If these functions are implemented separately in each place, it will lead to code redundancy, difficulty in maintenance, and duplication of work. Through unified function processing, these common functions can be extracted and processed in a unified way. This has several benefits:

  • “Code Reuse”: Extract common functions into independent modules or components, which can be shared and used in multiple places, reducing the workload of repeated code writing.

  • “Maintainability”: Common functions are centralized and can be easily modified, optimized or extended without the need to modify them in multiple places.

  • “Code cleanliness”: Through unified functional processing, the code can be made clearer and more concise, and redundant code can be reduced.

  • “Extensibility”: When new functions need to be added, modifications or extensions only need to be made where the unified function is processed, instead of modifications in multiple places, which reduces the coupling of the code. .

1 Unified user login authority verification

1.1 Difficulties in using native Spring AOP to implement unified interception

Taking the use of native Spring AOP to implement user system login verification as an example, it is mainly implemented using pre-notifications and surround notifications. The specific implementation is as follows

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * @author Huang Xiaohuang due to interest
 * @version 1.0
 * @date 2023/7/18 16:37
 */
@Aspect // Indicates that this class is an aspect
@Component // Started when the framework starts
public class UserAspect {
    //Define pointcuts, using aspect expression syntax here
    @Pointcut("execution(* com.hxh.demo.controller.UserController.*(..))")
    public void pointcut(){ }


    // pre-notification
    @Before("pointcut()")
    public void beforeAdvice() {
        System.out.println("Pre-notification executed~");
    }

    //surround notification
    @Around("pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
        System.out.println("Enter surround notification~");
        Object obj = null;
        //Execute target method
        try {
            obj = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("Exit surround notification~");
        return obj;
    }

}

As can be seen from the above code examples, the difficulties in using native Spring AOP to implement unified interception mainly include the following aspects:

  • Defining interception rules is very difficult. For example, the registration method and the login method are not intercepted. In this case, the rules for excluding the method are difficult to define, or even impossible to define.

  • It is difficult to get HttpSession in aspect classes.

In order to solve these problems of Spring AOP, Spring provides interceptors~

1.2 Use Spring interceptor to implement unified user login verification

Spring interceptor is a powerful component provided by the Spring framework for intercepting and processing requests before or after they reach the controller. Interceptors can be used to implement various functions such as authentication, logging, performance monitoring, etc.

To use Spring interceptors, you need to create an interceptor class that implements the HandlerInterceptor interface. This interface defines three methods: preHandle, postHandle and afterCompletion.

  • The preHandle method is executed before the request reaches the controller and can be used for authentication, parameter verification, etc.;

  • The postHandle method is executed after the controller has processed the request and can operate on the model and view;

  • The afterCompletion method is executed after the view rendering is completed and is used to clean up resources or record logs.

The implementation of the interceptor can be divided into the following two steps:

  • Create a custom interceptor and implement the preHandle (preprocessing before executing specific methods) method of the HandlerInterceptor interface.

  • Add custom interceptors to the addInterceptors method of WebMvcConfigurer and set interception rules.

The specific implementation is as follows:

step1. Create a custom interceptor. The custom interceptor is a common class. The code is as follows:

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * @author Huang Xiaohuang due to interest
 * @version 1.0
 * @date 2023/7/19 16:31
 * Unified user login permission verification - login interceptor
 */
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // User login business judgment
        HttpSession session = request.getSession(false);
        if (session != null & amp; & amp; session.getAttribute("userinfo") != null) {
            return true; // Verification successful, continue the controller process
        }
        // You can jump to the login interface or return 401/403 No permission code
        response.sendRedirect("/login.html"); // Jump to the login page
        return false; // Verification failed
    }
}

step2. Configure the interceptor and set interception rules. The code is as follows:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author Huang Xiaohuang due to interest
 * @version 1.0
 * @date 2023/7/19 16:51
 */
@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/**") //Intercept all requests
                .excludePathPatterns("/user/login") // URL addresses that are not blocked
                .excludePathPatterns("/user/reg")
                .excludePathPatterns("/**/*.html"); // Do not block all pages
    }
}
1.3 Implementation principle and source code analysis of interceptor

When there is an interceptor, corresponding business processing will be performed before calling the Controller. The execution process is shown in the figure below: 5d1bd55914882eb73b122635dc0243a0.png“Source code analysis of interceptor implementation principle”

It can be seen from the log information of the console of the above case implementation results that all Controller execution will be implemented through a scheduler DispatcherServlet. b55c266c9e3617378ab41642a9e4906e.pngAnd all methods will execute DispatcherServlet doDispatch scheduling method in doDispatch source code as follows:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;
            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null) {
                    this.noHandlerFound(processedRequest, response);
                    return;
                }
                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                String method = request.getMethod();
                boolean isGet = HttpMethod.GET.matches(method);
                if (isGet || HttpMethod.HEAD.matches(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) & amp; & amp; isGet) {
                        return;
                    }
                }
                
                //Call preprocessing
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
                //Execute business in Controller
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                this.applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }
            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        }
    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else if (multipartRequestParsed) {
            this.cleanupMultipart(processedRequest);
        }
    }
}

As can be seen from the above source code, before executing the Controller, the preprocessing method applyPreHandle will be called. The source code of this method is as follows:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    for(int i = 0; i <this.interceptorList.size(); this.interceptorIndex = i + + ) {
    // Get the interceptor HandlerInterceptor used in the project
        HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
        if (!interceptor.preHandle(request, response, this.handler)) {
            this.triggerAfterCompletion(request, response, (Exception)null);
            return false;
        }
    }
    return true;
}

In the above source code, it can be seen that all interceptor HandlerInterceptor will be obtained in applyPreHandle and the preHandle method in the interceptor will be executed. This corresponds to the steps we took to implement the interceptor before, as shown in the following figure: aef7a49175ce6e2cf4c35227102b9702.pngAt this time, the business logic in the corresponding preHandle will be executed.

1.4 Unified access prefix addition

The addition of a unified access prefix is similar to the login interceptor implementation, that is, adding the /hxh prefix to all request addresses. The sample code is as follows:

@Configuration
public class AppConfig implements WebMvcConfigurer {
    //Add /hxh prefix to all interfaces
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("/hxh", c -> true);
    }
}

Another way is to configure it in the application configuration file:

server.servlet.context-path=/hxh

2 Unified exception handling

Unified exception handling refers to defining a common exception handling mechanism in the application to handle all exception situations. This can avoid handling exceptions dispersedly in the application, reduce code complexity and duplication, and improve code maintainability and scalability.

Here are some points to consider:

  • Exception handling hierarchy: Define the exception handling hierarchy and determine which exceptions need to be handled uniformly and which exceptions need to be handed over to the upper layer for processing.

  • Exception handling methods: Determine how to handle exceptions, such as printing logs, returning error codes, etc.

  • Details of exception handling: Some details that need to be paid attention to when handling exceptions, such as whether transaction rollback is required, whether resources need to be released, etc.

The unified exception handling described in this article is implemented using @ControllerAdvice + @ExceptionHandler:

  • @ControllerAdvice represents the controller notification class.

  • @ExceptionHandler exception handler.

The above two annotations are used in combination to indicate that a certain notification is executed when an exception occurs, that is, a certain method event is executed. The specific implementation code is as follows:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;

/**
 * @author Huang Xiaohuang due to interest
 * @version 1.0
 * @date 2023/7/19 18:27
 * Unified exception handling
 */
@ControllerAdvice // Declaration is an exception handler
public class MyExHandler {

    //Intercept all null pointer exceptions and return unified data
    @ExceptionHandler(NullPointerException.class) // Uniformly handle null pointer exceptions
    @ResponseBody // Return data
    public HashMap<String, Object> nullException(NullPointerException e) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", "-1"); //Exception status code defined with the front end
        result.put("msg", "Null pointer exception: " + e.getMessage()); // Description of the error code
        result.put("data", null); //Returned data
        return result;
    }
}

In the above code, all null pointer exceptions are intercepted and uniform data is returned.

In practice, a guarantee is often set. For example, if a non-null pointer exception occurs, there will also be guarantee measures to handle it, similar to using Exception in a try-catch block to capture. The code example is as follows:

@ExceptionHandler(Exception.class)
@ResponseBody
public HashMap<String, Object> exception(Exception e) {
    HashMap<String, Object> result = new HashMap<>();
    result.put("code", "-1"); //Exception status code defined with the front end
    result.put("msg", "Exception: " + e.getMessage()); // Description of the error code
    result.put("data", null); //Returned data
    return result;
}

3 Unified data return format

In order to maintain the consistency and ease of use of the API, it is usually necessary to use a unified data return format. Generally speaking, a standard data return format should include the following elements:

  • Status code: status information used to mark the success or failure of the request;

  • Message: specific information used to describe the request status;

  • Data: Contains requested data information;

  • Timestamp: The time information of the request can be recorded to facilitate debugging and monitoring.

To achieve a unified data return format, you can use @ControllerAdvice + ResponseBodyAdvice. The specific steps are as follows:

  • Create a class and add the @ControllerAdvice annotation;

  • Implement the ResponseBodyAdvice interface and override the supports and beforeBodyWrite methods.

The sample code is as follows:

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;

import java.util.HashMap;

/**
 * @author Huang Xiaohuang due to interest
 * @version 1.0
 * @date 2023/7/19 18:59
 * Unified data return format
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    /**
     * If this method returns true, the following beforeBodyWrite method will be executed, otherwise it will not be executed.
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    /**
     * Call this method before returning
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "");
        result.put("data", body);
        return null;
    }
}

However, if the original data type of the returned body is String, a type conversion exception, namely ClassCastException, will occur.

Therefore, if the original return data type is String, you need to use jackson for separate processing. The implementation code is as follows:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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;

import java.util.HashMap;

/**
 * @author Huang Xiaohuang due to interest
 * @version 1.0
 * @date 2023/7/19 18:59
 * Unified data return format
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * If this method returns true, the following beforeBodyWrite method will be executed, otherwise it will not be executed.
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    /**
     * Call this method before returning
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("msg", "");
        result.put("data", body);
        if (body instanceof String) {
            // Requires special handling of String
            try {
                return objectMapper.writeValueAsString(result);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}

However, in actual business, the above code is only used as a guarantee, because the status code always returns 200, which is too rigid and requires detailed analysis of specific problems.

Source: blog.csdn.net/m0_60353039/

article/details/131810657

Back-end exclusive technology group

To build a high-quality technical exchange community, HR personnel engaged in programming development and technical recruitment are welcome to join the group. Everyone is also welcome to share their own company’s internal information, help each other, and make progress together!

Speech in a civilized manner, focusing on communication technology, recommendation of positions, and industry discussion

Advertisers are not allowed to enter, and do not trust private messages to prevent being deceived.

746a855581b8a6c5be45d211fda55a1c.png

Add me as a friend and bring you into the group
The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java Skill TreeHomepageOverview 137051 people are learning the system