Spring Boot unified transaction processing (interceptor)

1. What is an interceptor?

1.1 Meaning

In Spring Boot, an interceptor is a mechanism for intercepting and processing HTTP requests. It is a middleware provided by the Spring framework to execute some shared logic before or after the request reaches the controller (Controller).

The interceptor of Spring Boot is implemented based on the HandlerInterceptor interface in the Spring MVC framework. By creating a custom interceptor class and implementing the HandlerInterceptor interface, you can define the logic and behavior to be executed by the interceptor.

1.2 Function

Authentication and permission control: Interceptors can be used to check the user’s authentication status and permissions, and perform related processing as needed. For example, an interceptor can be used to verify the user’s login status, and if not, redirect to the login page or return an appropriate error message.

Exception handling and unified error handling: Interceptors can capture and handle exceptions that occur during request processing. Appropriate handling can be performed according to the exception type, such as returning a custom error page or error message, or executing specific error handling logic.

Of course it has other application scenarios, so I won’t list them here.

2. ?User login authority verification

2.1 Custom Interceptor

java copy code @Component
public class LoginInterceptor implements HandlerInterceptor {

    //The method executed before calling the target method
    //If true is returned, the interceptor verification is successful, and the target method is executed
    //If false is returned, it means that the interceptor verification failed, and no subsequent business will be performed
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //User login judgment business
        HttpSession session = request. getSession(false);

        if(session != null & amp; & amp; session. getAttribute("session_userinfo") != null){
            //User is logged in
            return true;
        }
        response. setStatus(401);
        return false;
    }
}

The preHandle method in the code is the main method of the interceptor and is executed before the target method is called. It receives three parameters: the HttpServletRequest object represents the current HTTP request, the HttpServletResponse object represents the current HTTP response, and the Object handler represents the intercepted processor (usually a method in the Controller).

In the preHandle method, first obtain the HttpSession object of the current request (if it exists) through request.getSession(false), and then judge whether the HttpSession object is null and whether there is an attribute named “session_userinfo”. If this condition is true, it means that the user has logged in and can continue to perform subsequent services, so return true, otherwise the verification fails, set the status code of the HTTP response to 401, indicating unauthorized, then return false, and do not continue to perform subsequent services.

2.2 Add the defined interceptor to the system configuration

java copy code @Configuration
public class MyConfig implements WebMvcConfigurer {

    //injection
    @Autowired
    private LoginInterceptor loginInterceptor;

    // set the interceptor
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry. addInterceptor(loginInterceptor)
                .addPathPatterns("/**") //Intercept all urls
                .excludePathPatterns("/user/login")//exclude url: /user/login (login)
                .excludePathPatterns("/user/reg") //Exclude url: /user/reg (registration)
                .excludePathPatterns("/image/**")//Exclude all files under the image (image) folder
                .excludePathPatterns("/**/*.js")//Exclude all ".js" files in any depth directory
                .excludePathPatterns("/**/*.css");
    }
}

In the configuration class, the addInterceptors method is rewritten, which is used to register interceptors. Here, the interceptor is added by calling the addInterceptor method of the InterceptorRegistry, and the intercepted path and the excluded path are set.

Specifically, add the LoginInterceptor interceptor by calling addInterceptor(loginInterceptor). Then use the addPathPatterns method to specify the URL path pattern that needs to be intercepted. Here, “/**” is used to indicate that all URLs are intercepted. Use the excludePathPatterns method to exclude some specific URL paths that will not be blocked.

For “/**/*.js”, “**”: indicates zero or more path segments (directories or folders), which can match any directory structure of any depth. “/*.js”: Indicates the file name ending with “.js”.

UserController:

java copy code @RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/login")
    public String login(){
        return "login";
    }

    @RequestMapping("/index")
    public String index(){
        return "index";
    }

    @RequestMapping("/reg")
    public String reg(){
        return "reg";
    }
}

Visit "login":

Visit "index":

returned 401

Visit "reg":

2.3 System? Access prefix added

Add test prefix to all request addresses: In the WebMvcConfigurer interface, the configurePathMatch method is used to configure path matching rules.

javaCopy code
@Configuration
public class MyConfig implements WebMvcConfigurer {
        //Add unified access prefix
        @Override
        public void configurePathMatch(PathMatchConfigurer configurer) {
            configurer.addPathPrefix("test", new Predicate<Class<?>>() {
                @Override
                public boolean test(Class<?> aClass) {
                    return true;
                }
            });
        }
}

In this example, the prefix passed to the addPathPrefix method is “test”, and the Predicate object is an anonymous inner class that implements the Predicate> interface. The Predicate interface is a functional interface introduced in Java 8, and its test method is used to determine whether the incoming class meets the conditions.

In this anonymous inner class, the test method is rewritten to always return true, which means that all classes are eligible and will be prefixed with a unified access.

Therefore, through the configuration of this code, all request paths will be prefixed with “test”. For example, the original path is “/example”, and the path after adding the prefix becomes “/test/example”. This enables unified processing of the request path.

Note: If a prefix is added, the exclusion path of the interceptor should also be changed accordingly:

java copy code //Insert the interceptor
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry. addInterceptor(loginInterceptor)
                .addPathPatterns("/**") //Intercept all urls
                .excludePathPatterns("/**/user/login")//exclude url: /user/login (login)
                .excludePathPatterns("/**/user/reg") //Exclude url: /user/reg (registration)
                .excludePathPatterns("/**/image/**")//Exclude all files under the image (image) folder
                .excludePathPatterns("/**/*.js")//Exclude all ".js" files in any depth directory
                .excludePathPatterns("/**/*.css");
    }

3. System? Exception handling

What will the code below return after accessing it?

java copy code @RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public Integer login(){
        Object object = null;
        object.hashCode();
        return 1;
    }
}

the answer is:

Is there a way to return useful information when an exception occurs (the response status at this time is 200), instead of cold error messages?

That is the unified exception handling:

java copy code @ControllerAdvice
@ResponseBody
public class MyExceptionAdvice {
    @ExceptionHandler(NullPointerException. class)
    public HashMap<String, Object> doNullPointerException(NullPointerException e){
        HashMap<String,Object> result = new HashMap<>();
        result. put("code",-1);
        result.put("msg","null pointer:" + e.getMessage());
        result. put("data",null);
        return result;
    }
}
  1. The @ControllerAdvice annotation identifies this class as a global exception handler, which will catch exceptions thrown in the application and execute corresponding processing logic.
  2. The @ExceptionHandler(NullPointerException.class) annotation specifies the method doNullPointerException() for handling NullPointerException type exceptions.
  3. The parameter of the doNullPointerException() method is an exception object of the NullPointerException type, which represents the specific exception instance caught.
  4. The doNullPointerException() method returns a HashMap object for encapsulating the exception handling result.

The function of this code is to execute the doNullPointerException() method when a NullPointerException is caught, and return a HashMap object containing the exception handling result. The result is returned to the client in JSON format.

When there are multiple exception handlers, their processing order:

java copy code @ControllerAdvice
@ResponseBody
public class MyExceptionAdvice {

    // Handle NullPointerException exception
    @ExceptionHandler(NullPointerException. class)
    public HashMap<String, Object> doNullPointerException(NullPointerException e){
        //deal with
        HashMap<String,Object> result = new HashMap<>();
        result. put("code",-1);
        result. put("msg","NullPointerException: " + e. getMessage());
        result. put("data",null);
        return result;
    }
    
// Handle Exception exception
    @ExceptionHandler(Exception. class)
    public HashMap<String, Object> doException(Exception e){
        //deal with
        HashMap<String,Object> result = new HashMap<>();
        result. put("code",-1);
        result. put("msg","Exception: " + e. getMessage());
        result. put("data",null);
        return result;
    }
}
java copy code @RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public Integer login(){
        Object object = null;
        object.hashCode();
        return 1;
    }
}

Conclusion: If there is a match, the subclass takes precedence; if there is no match, then the parent class is found.

4. Unified data return format

4.1 Why is it necessary to unify the data return format?

  1. The unified data return format can help front-end programmers better receive and parse the data returned by the back-end data interface,
  2. Standardization and readability: The unified data return format can define a unified data structure and fields, so that the returned data of different interfaces are consistent. This can improve the readability and maintainability of the code, and reduce the communication cost between front-end and back-end developers.
  3. Unified data maintenance and modification: By unifying the data return format, all interfaces in the project follow the same data format, making data maintenance and modification more convenient and unified. If you need to modify the structure or fields returned by the data, you only need to modify it at the definition of the unified data return format, instead of modifying the interface one by one.
  4. Conducive to the formulation of back-end technical specification standards: the unified data return format can be used as a specification standard formulated by the back-end technical department. It can avoid all kinds of strange return content, provide a unified data specification and structure, enable developers to better follow the specification during the back-end development process, and improve code quality and maintainability.

4.2 Unified data return format case

Suppose here we use {“msg”: *,”code”: *,”data”: *} as the standard return format. Using the @ControllerAdvice annotation combined with the ResponseBodyAdvice interface can achieve a unified data return format.

java copy code @ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    /**
     * @return If true, execute beforeBodyWrite
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType){
        return true;
    }

    /**
     * Data rewriting before returning data
     * @param body original return value
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType, Class selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {

// Process before returning the data, you can modify, wrap the response body, add additional information, etc.
        // Here you can perform unified format processing on the returned data
        if(body instanceof HashMap){
            // If it is already a unified response format, return directly
            return body;
        }
        // If it is not a unified response format, wrap it
        HashMap<String,Object> result = new HashMap<>();
        result. put("code",200);
        result. put("data", body);
        result. put("msg","");
        return result;
    }
}
java copy code @RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public HashMap<String, Object> login(){
        HashMap<String,Object> hashMap = new HashMap<>();
        hashMap.put("code",200);
        hashMap. put("data",1);
        hashMap.put("msg","");
        return hashMap;
    }
    @RequestMapping("/reg")
    public Integer reg(){
        return 10;
    }
}

4.3 The return value is String

If I want to unify the format of the following code:

java copy code @RequestMapping("/hi")
public String hi(){
    return "Hello World";
}

It can be found that an error was reported, why didn’t it succeed?

Return to the execution flow:

  1. The method returns a String
  2. Processing before the unified data is returned: load the String into the HashMap.
  3. Convert HashMap to application/json string for front end.

The problem lies in the third step. When converting application/json, the original Body type will be judged first. If it is String type, StringHttpMessageConverter will be enabled for type conversion. If it is not String type, HttpMessageConverter will be used. convert.

In Spring MVC, HttpMessageConverter is responsible for processing the message body of the request and response, converting the requested data into the type of the method parameter, and converting the method return value into the response data format. StringHttpMessageConverter is especially designed to handle string type data.

StringHttpMessageConverter can only convert String to other types, such as returning the string to the client as the content of the response. In the code, the HashMap is converted to an application/json string. So it was wrong.

Solution:

  • Do special processing directly on String: directly convert HashMap into json string and send it to the front end.
java copy code @ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {

    @Autowired
    private ObjectMapper objectMapper;

    ?…
        slightly
    ?…
        
    //rewrite the return result
    HashMap<String,Object> result = new HashMap<>();
    result. put("code",200);
    result. put("data", body);
    result. put("msg","");
    if(body instanceof String){
        //convert HashMap to json string
        return objectMapper.writeValueAsString(request);
    }

    ?…
        slightly
    ?…
}
  • Remove StringHttpMessageConverter
java copy code @Configuration
public class MyConfig implements WebMvcConfigurer {
    /**
     * Remove StringHttpMessageConverter
     *
     * @param converters
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters. removeIf(converter -> converter instanceof StringHttpMessageConverter);
    }
}