[Spring Boot] interceptor and unified function processing: unified login verification, unified exception handling and unified data return format

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 target objects through proxy objects before and after method execution, throwing Execute at a specific location when an exception occurs or when a result is 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 for unified function processing, which is also an actual battle of AOP, as follows:

  • Unified user login authority verification
  • Unified data format return
  • Unified exception handling


0 Why is unified function processing needed?

Unified function processing is a design idea to improve code maintainability, reusability and scalability. 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 labor. By means of unified function processing, these common functions can be extracted and processed in a unified manner. This has several advantages:

  1. 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.
  2. Maintainability: Centralize common functions so that they can be easily modified, optimized or extended without modification in multiple places.
  3. Code cleanliness: Through unified function processing, the code can be made clearer and more concise, and redundant code can be reduced.
  4. Scalability: When it is necessary to add new functions, it only needs to be modified or expanded in the place where the unified function is processed, instead of in multiple places, which reduces the coupling of the code.

expression


1 Unified user login authority verification

1.1 Difficulties in implementing unified interception using native Spring AOP

In the article: [Spring] Introduction to Spring AOP and Analysis of Implementation Principles , we learned how to use AOP for aspect-oriented programming. Take the use of native Spring AOP to implement unified user login verification as an example, mainly using pre-notification and surround notification. The specific implementation is as follows:

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

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/18 16:37
 */
@Aspect // 表明此类为一个切面
@Component // 随着框架的启动而启动
public class UserAspect {
    
    
    // 定义切点, 这里使用 Aspect 表达式语法
    @Pointcut("execution(* com.hxh.demo.controller.UserController.*(..))")
    public void pointcut(){
    
     }


    // 前置通知
    @Before("pointcut()")
    public void beforeAdvice() {
    
    
        System.out.println("执行了前置通知~");
    }

    // 环绕通知
    @Around("pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
    
    
        System.out.println("进入环绕通知~");
        Object obj = null;
        // 执行目标方法
        try {
    
    
            obj = joinPoint.proceed();
        } catch (Throwable e) {
    
    
            e.printStackTrace();
        }
        System.out.println("退出环绕通知~");
        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:

  1. Defining interception rules is very difficult. For example, the registration method and login method are not intercepted. In this case, the rules for the exclusion method are difficult to define, or even impossible to define.
  2. It is more difficult to get HttpSession in the aspect class.

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

1.2 Using Spring Interceptor to Realize Unified User Login Verification

The Spring interceptor is a powerful component provided by the Spring framework, which is used to intercept and process the request before or after it reaches 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 HandlerInterceptorinterceptor class that implements the interface. The interface defines three methods: preHandle, postHandleand afterCompletion. preHandleThe method is executed before the request reaches the controller, which can be used for authentication, parameter verification, etc.; postHandlethe method is executed after the controller processes the request, and can operate on the model and view; afterCompletionthe method is executed after the view rendering is completed, and is used for cleaning resource or logging.

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

  1. Create a custom interceptor to implement the method HandlerInterceptorof the interface preHandle(preprocessing before executing the specific method).
  2. Add the custom interceptor WebMvcConfigurerto addInterceptorsthe method of and set the interception rules.

The specific implementation is as follows:

step1. Create a custom interceptor, which is a common class, and 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 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 16:31
 * 统一用户登录权限验证 —— 登录拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
    
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        // 用户登录业务判断
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("userinfo") != null) {
    
    
            return true; // 验证成功, 继续controller的流程
        }
        // 可以跳转登录界面或者返回 401/403 没有权限码
        response.sendRedirect("/login.html"); // 跳转到登录页面
        return false; // 验证失败
    }
}

step2. Configure the interceptor and set the 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 兴趣使然黄小黄
 * @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("/**") // 拦截所有请求
                .excludePathPatterns("/user/login") // 不拦截的 url 地址
                .excludePathPatterns("/user/reg")
                .excludePathPatterns("/**/*.html"); // 不拦截所有页面
    }
}

1.3 Implementation principle and source code analysis of the interceptor

When there is an interceptor, it will perform corresponding business processing before calling the Controller. The execution process is shown in the following figure:
Implementation process
Source code analysis of the principle of interceptor implementation
From the log information of the console of the implementation results of the above case, it can be seen that all Controller execution will be realized through a dispatcher DispatcherServlet.
achieve results
And all methods will execute the doDispatch scheduling method in DispatcherServlet. The source code of doDispatch is 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) && isGet) {
    
    
                        return;
                    }
                }
                
                // 调用预处理
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    
    
                    return;
                }
                // 执行 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);
        }
    }
}

It can be seen from the above source code that before executing the Controller, the preprocessing method applyPreHandle will be called first. 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++) {
    
    
    // 获取项目中使用的拦截器 HandlerInterceptor
        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 in applyPreHandle, all interceptors HandlerInterceptor will be obtained and the preHandle method in the interceptor will be executed, which corresponds to the steps we used to implement the interceptor before, as shown in the figure below: At this time, in the corresponding
implementation details
preHandle The business logic will be executed.

1.4 Adding a unified access prefix

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

@Configuration
public class AppConfig implements WebMvcConfigurer {
    
    
    // 给所有接口添加 /hxh 前缀
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
    
    
        configurer.addPathPrefix("/hxh", c -> true);
    }
}

Another way is to configure 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 exceptions. In this way, it is possible to avoid scattered handling of exceptions in the application, reduce the complexity and repetition of the code, and improve the maintainability and scalability of the code.

The following points need to be considered:

  1. Hierarchical structure of exception handling: Define the hierarchical structure of exception handling, determine which exceptions need to be handled uniformly, and which exceptions need to be handed over to the upper layer for processing.

  2. Exception handling method: determine how to handle exceptions, such as printing logs, returning error codes, and so on.

  3. 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

  • @ControllerAdviceRepresents a controller notification class.
  • @ExceptionHandlerexception handler.

The above two annotations are used in combination to indicate that a notification is executed when an exception occurs, that is, a 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 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:27
 * 统一异常处理
 */
@ControllerAdvice // 声明是一个异常处理器
public class MyExHandler {
    
    

    // 拦截所有的空指针异常, 进行统一的数据返回
    @ExceptionHandler(NullPointerException.class) // 统一处理空指针异常
    @ResponseBody // 返回数据
    public HashMap<String, Object> nullException(NullPointerException e) {
    
    
        HashMap<String, Object> result = new HashMap<>();
        result.put("code", "-1"); // 与前端定义好的异常状态码
        result.put("msg", "空指针异常: " + e.getMessage()); // 错误码的描述信息
        result.put("data", null); // 返回的数据
        return result;
    }
}

In the above code, the interception of all null pointer exceptions and unified data return are realized.

In practice, a guarantee is often set. For example, if a non-null pointer exception occurs, there will also be guarantee measures to deal with it, similar to using Exception in the try-catch block to catch. 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"); // 与前端定义好的异常状态码
    result.put("msg", "异常: " + e.getMessage()); // 错误码的描述信息
    result.put("data", null); // 返回的数据
    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 status of the request;
  • Data: contains the requested data information;
  • Timestamp: The time information of the request can be recorded, which is convenient for debugging and monitoring.

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

  1. Create a class and add @ControllerAdviceannotations;
  2. Implement ResponseBodyAdvicethe 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 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:59
 * 统一数据返回格式
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    
    

    /**
     * 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
    
    
        return true;
    }

    /**
     * 方法返回之前调用此方法
     */
    @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 returned body primitive data type is String, a type conversion exception will occur, ie ClassCastException.

Therefore, if the original return data type is String, you need to use jackson for separate processing, and 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 兴趣使然黄小黄
 * @version 1.0
 * @date 2023/7/19 18:59
 * 统一数据返回格式
 */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    
    

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 此方法返回 true 则执行下面的 beforeBodyWrite 方法, 反之则不执行
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
    
    
        return true;
    }

    /**
     * 方法返回之前调用此方法
     */
    @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) {
    
    
            // 需要对 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 specific analysis of specific issues.


write at the end

This article is included in the JavaEE programming road点击订阅专栏 and is being updated continuously.
 The above is the whole content of this article! Creation is not easy, if you have any questions, welcome to private message, thank you for your support!

insert image description here

Guess you like

Origin blog.csdn.net/m0_60353039/article/details/131810657