Interceptor [Spring Boot] y procesamiento de funciones unificadas: verificación de inicio de sesión unificada, manejo de excepciones unificado y formato de retorno de datos unificado

prefacio

 Spring AOP es un marco basado en la programación orientada a aspectos, que se utiliza para separar las preocupaciones transversales (como el registro, la gestión de transacciones) de la lógica comercial y tejer estas preocupaciones en los objetos de destino a través de objetos proxy antes y después de la ejecución del método, lanzando Ejecutar en una ubicación específica cuando ocurre una excepción o cuando se devuelve un resultado, mejorando así la reutilización, mantenibilidad y flexibilidad del programa. Sin embargo, es muy engorroso y difícil implementar la intercepción unificada utilizando Spring AOP nativo. En esta sección, utilizaremos un método simple para el procesamiento de funciones unificadas, que también es una batalla real de AOP, de la siguiente manera:

  • Verificación de autoridad de inicio de sesión de usuario unificado
  • Devolución de formato de datos unificado
  • Manejo unificado de excepciones


0 ¿Por qué es necesario el procesamiento de funciones unificadas?

El procesamiento de funciones unificadas es una idea de diseño para mejorar la capacidad de mantenimiento, la reutilización y la escalabilidad del código. En una aplicación, puede haber algunos requisitos funcionales comunes, como autenticación, registro, manejo de excepciones, etc. Estas funciones deben llamarse y procesarse en varios lugares. Si estas funciones se implementan por separado en cada lugar, dará lugar a redundancia de código, dificultad en el mantenimiento y duplicación de trabajo. Mediante el procesamiento de funciones unificadas, estas funciones comunes se pueden extraer y procesar de manera unificada. Esto tiene varias ventajas:

  1. Reutilización de código: extraiga funciones comunes en módulos o componentes independientes, que se pueden compartir y usar en varios lugares, lo que reduce la carga de trabajo de la escritura de código repetida.
  2. Capacidad de mantenimiento: centralice las funciones comunes para que puedan modificarse, optimizarse o ampliarse fácilmente sin modificaciones en varios lugares.
  3. Limpieza del código: a través del procesamiento de funciones unificadas, el código se puede hacer más claro y conciso, y se puede reducir el código redundante.
  4. Escalabilidad: Cuando es necesario agregar nuevas funciones, solo se necesita modificar o expandir en el lugar donde se procesa la función unificada, en lugar de en múltiples lugares, lo que reduce el acoplamiento del código.

expresión


1 Verificación de autoridad de inicio de sesión de usuario unificado

1.1 Dificultades en la implementación de la intercepción unificada usando Spring AOP nativo

En el artículo: [Spring] Introducción a Spring AOP y análisis de los principios de implementación , aprendimos cómo usar AOP para la programación orientada a aspectos. Tome el uso de Spring AOP nativo para implementar la verificación unificada de inicio de sesión de usuario como ejemplo, principalmente mediante notificación previa y notificación envolvente. La implementación específica es la siguiente:

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;
    }

}

Como se puede ver en los ejemplos de código anteriores, las dificultades para usar Spring AOP nativo para implementar la intercepción unificada incluyen principalmente los siguientes aspectos:

  1. Definir reglas de interceptación es muy difícil. Por ejemplo, el método de registro y el método de inicio de sesión no se interceptan, por lo que es difícil definir las reglas del método de exclusión, o incluso imposible de definir.
  2. Es más difícil obtener HttpSession en la clase de aspecto.

Para resolver estos problemas de Spring AOP, Spring proporciona interceptores ~

1.2 Uso de Spring Interceptor para realizar la verificación de inicio de sesión de usuario unificado

El interceptor Spring es un componente poderoso proporcionado por el marco Spring, que se utiliza para interceptar y procesar la solicitud antes o después de que llegue al controlador. Los interceptores se pueden utilizar para implementar diversas funciones, como la autenticación, el registro, la supervisión del rendimiento, etc.

Para usar los interceptores de Spring, debe crear una HandlerInterceptorclase de interceptor que implemente la interfaz. La interfaz define tres métodos: preHandle, postHandley afterCompletion. preHandleEl método se ejecuta antes de que la solicitud llegue al controlador, que se puede usar para autenticación, verificación de parámetros, etc.; postHandleel método se ejecuta después de que el controlador procesa la solicitud y puede operar en el modelo y la vista; afterCompletionel método se ejecuta después de que el la representación de la vista se completa y se usa para limpiar recursos o iniciar sesión.

La implementación del interceptor se puede dividir en los siguientes dos pasos:

  1. Cree un interceptor personalizado para implementar el método HandlerInterceptorde la interfaz preHandle(preprocesamiento antes de ejecutar el método específico).
  2. Agregue el interceptor personalizado WebMvcConfigureral addInterceptorsmétodo de y configure las reglas de interceptación.

La implementación específica es la siguiente:

paso 1. Cree un interceptor personalizado, que es una clase común, y el código es el siguiente:

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; // 验证失败
    }
}

Paso 2. Configure el interceptor y establezca las reglas de interceptación, el código es el siguiente:

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 Principio de implementación y análisis del código fuente del interceptor

Cuando hay un interceptor, realizará el procesamiento comercial correspondiente antes de llamar al Controlador.El proceso de ejecución se muestra en la siguiente figura:
Proceso de implementación
Análisis de código fuente del principio de implementación del interceptor
De la información de registro de la consola de los resultados de implementación de lo anterior caso, se puede ver que toda la ejecución del controlador se realizará a través de un despachador DispatcherServlet.
alcanzar resultados
Y todos los métodos ejecutarán el método de programación doDispatch en DispatcherServlet.El código fuente de doDispatch es el siguiente:

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);
        }
    }
}

Se puede ver en el código fuente anterior que antes de ejecutar el controlador, primero se llamará al método de preprocesamiento applyPreHandle El código fuente de este método es el siguiente:

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;
}

En el código fuente anterior, se puede ver que en applyPreHandle, se obtendrán todos los interceptores HandlerInterceptor y se ejecutará el método preHandle en el interceptor, que corresponde a los pasos que usamos para implementar el interceptor antes, como se muestra en la figura a continuación. : En este momento, en el
detalles de implementacion
preHandle correspondiente se ejecutará la lógica de negocio.

1.4 Agregar un prefijo de acceso unificado

La adición del prefijo de acceso unificado es similar a la implementación del interceptor de inicio de sesión, es decir, agrega el prefijo /hxh a todas las direcciones de solicitud. El código de muestra es el siguiente:

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

Otra forma es configurar en el archivo de configuración de la aplicación:

server.servlet.context-path=/hxh

2 Manejo unificado de excepciones

El manejo unificado de excepciones se refiere a la definición de un mecanismo común de manejo de excepciones en la aplicación para manejar todas las excepciones. De esta forma, es posible evitar el manejo disperso de excepciones en la aplicación, reducir la complejidad y repetición del código y mejorar la mantenibilidad y escalabilidad del código.

Es necesario considerar los siguientes puntos:

  1. Estructura jerárquica del manejo de excepciones: defina la estructura jerárquica del manejo de excepciones, determine qué excepciones deben manejarse de manera uniforme y qué excepciones deben transferirse a la capa superior para su procesamiento.

  2. Método de manejo de excepciones: determine cómo manejar las excepciones, como imprimir registros, devolver códigos de error, etc.

  3. Detalles del manejo de excepciones: algunos detalles a los que se debe prestar atención cuando se manejan excepciones, como si se requiere revertir la transacción, si es necesario liberar recursos, etc.

El manejo unificado de excepciones descrito en este artículo se implementa usando + @ControllerAdvice:@ExceptionHandler

  • @ControllerAdviceRepresenta una clase de notificación de controlador.
  • @ExceptionHandlermanejador de excepciones

Las dos anotaciones anteriores se usan en combinación para indicar que se ejecuta una notificación cuando ocurre una excepción, es decir, se ejecuta un evento de método. El código de implementación específico es el siguiente:

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;
    }
}

En el código anterior, se realizan la intercepción de todas las excepciones de puntero nulo y el retorno de datos unificado.

En la práctica, a menudo se establece una garantía. Por ejemplo, si se produce una excepción de puntero no nulo, también habrá medidas de garantía para tratarla, similar al uso de Exception en el bloque try-catch para capturar. El ejemplo de código es como sigue:

@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 Formato de devolución de datos unificado

Para mantener la coherencia y la facilidad de uso de la API, normalmente es necesario utilizar un formato de devolución de datos unificado. En términos generales, un formato estándar de devolución de datos debe incluir los siguientes elementos:

  • Código de estado: información de estado utilizada para marcar el éxito o fracaso de la solicitud;
  • Mensaje: información específica utilizada para describir el estado de la solicitud;
  • Datos: contiene la información de los datos solicitados;
  • Marca de tiempo: se puede registrar la información de tiempo de la solicitud, lo cual es conveniente para la depuración y el monitoreo.

Para lograr un formato de devolución de datos unificado, puede usar el método @ControllerAdvice+ ResponseBodyAdvicepara lograrlo. Los pasos específicos son los siguientes:

  1. Crea una clase y agrega @ControllerAdviceanotaciones;
  2. Implemente ResponseBodyAdvicela interfaz y anule los métodos support y beforeBodyWrite.

El código de ejemplo es el siguiente:

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;
    }
}

Sin embargo, si el tipo de datos primitivo del cuerpo devuelto es String, se producirá una excepción de conversión de tipo, es decir, ClassCastException.

Por lo tanto, si el tipo de datos de retorno original es String, debe usar jackson para un procesamiento separado y el código de implementación es el siguiente:

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;
    }
}

Sin embargo, en los negocios reales, el código anterior solo se usa como garantía, porque el código de estado siempre devuelve 200, que es demasiado rígido y requiere un análisis específico de problemas específicos.


escribir al final

Este artículo está incluido en el camino de la programación JavaEE点击订阅专栏 y se actualiza continuamente.
 ¡Lo anterior es todo el contenido de este artículo! La creación no es fácil, si tiene alguna pregunta, bienvenido a un mensaje privado, ¡gracias por su apoyo!

inserte la descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/m0_60353039/article/details/131810657
Recomendado
Clasificación