Meituan Technology | Cómo registrar registros de operaciones con elegancia

Si tiene alguna pregunta durante el uso, busque la cuenta pública de WeChat: ¡Du Xiaotou , el programador, y contácteme! ! !

Escanear código_Buscar estilo de comunicación conjunta-versión de color estándar.jpg

"Cómo registrar registros operativos con elegancia" es el artículo técnico más popular del equipo técnico de Meituan en 2021. El artículo es muy detallado y se recomienda encarecidamente que lo lea. El registro de operaciones se refiere a quién hizo qué a qué en un momento determinado. El registro de operaciones generalmente se limita a operaciones de creación, actualización y eliminación, y la consulta no es una operación confidencial, por lo que no es necesario registrar el registro de operaciones. Por ejemplo: el administrador agregó un usuario en 2020-10-10 11:12:13, el nombre de usuario es crimson_typhoon; el comprador Jade Bird actualizó el correo electrónico de contacto en 2020-10-10 11:12:13, antes de la actualización: 111111@qq .com , después de la actualización: [email protected], etc.

Aunque el texto original se ha escrito en profundidad, puede estar limitado por el espacio y no se ha elaborado el conocimiento relacionado con Spring AOP involucrado en las anotaciones de métodos , por lo que este artículo se centrará en este contenido.

Para ser honesto, es una solución extremadamente elegante para implementar registros de operaciones basados ​​en anotaciones de métodos. Puede converger efectivamente la lógica de preocupaciones transversales, evitar la lógica de registro de operaciones de registro dispersas en varias clases de negocios y mejorar en gran medida la legibilidad y mantenibilidad del código sexo. Algunos controladores antiguos pueden pensar que está bien usarlo directamente Aspectj, como el código publicado a continuación. Es posible hacer esto, pero no es fácil reutilizarlo más tarde y hay problemas de compatibilidad .

@Component
@Aspect
public class OperationLogAdvice {
    @Around(value = "@annotation(io.github.oplog.annotation.OperationLog)")
    public Object doOperationLog(ProceedingJoinPoint joinPoint) {
        // STEP 1:执行目标方法
        Object result = null;
        Throwable exceptionOnTraget = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            exceptionOnTraget = e;
        }
        // STEP 2:记录操作日志
        // STEP 3:如果目标方法执行失败,那么需要重新抛出异常
        return result;
    }
}
复制代码

cómo utilizar

Haciendo referencia a las ideas del texto original, el autor implementa un componente que registra los registros de operaciones; este componente se ha liberado al almacén central de maven y puede experimentarlo. La información del GAV es la siguiente:

<dependency>
   <groupId>io.github.dk900912</groupId>
   <artifactId>oplog-spring-boot-starter</artifactId>
   <version>1.0.5</version>
</dependency>
复制代码

El código de ejemplo es el siguiente:

@RestController
@RequestMapping(path = "/demo/v1")
public class DemoController {
    private OperationLogDao operationLogDao;

    public DemoController(OperationLogDao operationLogDao) {
        this.operationLogDao = operationLogDao;
    }

    @OperationLog(bizCategory = BizCategory.CREATE, bizTarget = "订单", bizNo = "#AppResult.data.orderId")
    @PostMapping(path = "/create")
    public AppResult create(@RequestBody OrderReq orderReq) {
        // 创建资源时,一般前端不会将ID传过来,所以一般都是从响应结果中获取bizNo哈
        orderReq.setOrderId("001");
        return AppResult.success().data(orderReq);
    }

    @OperationLog(bizCategory = BizCategory.UPDATE, bizTarget = "订单", bizNo = "#orderReq.orderId")
    @PutMapping(path = "/update")
    public AppResult update(@RequestBody OrderReq orderReq) {
        return AppResult.success();
    }

    @OperationLog(bizCategory = BizCategory.DELETE, bizTarget = "订单", bizNo = "#id")
    @DeleteMapping(path = "/delete/{id}")
    public AppResult delete(@PathVariable("id") int id) {
        return AppResult.success();
    }
}
复制代码

如果业务方法A调用了业务方法B,且A和B这俩方法都由@OperationLog标记,那么B方法中并不会记录操作日志,这是Spring AOP的老问题了,官方也提供了解决方法,比如使用AopContext.currentProxy()

代理对象如何生成

基于方法注解实现操作日志这一方案需要依赖于Spring AOP,核心思想就是借助Spring AOP去自动探测由OperationLog注解接口标记的业务逻辑类,从而为这些业务类动态创建代理对象。既然Spring AOP是基于代理对象来拓展目标对象的,那就很容易想到:Spring IoC容器内贮存的一定是代理对象而非目标对象,那究竟是如何替换的呢?众所周知,Spring暴露了若干IoC容器拓展点(IoC Container Extensiion Points),BeanPostProcessor接口就是其中之一;有了BeanPostProcessor,任何人都可以在Bean初始化前后对其进行个性化改造,甚至将其替换。

让我们来看一下BeanPostProcessor接口中的内容,它只有两个方法,如下:

public interface BeanPostProcessor {
    default Object postProcessBeforeInitialization(Object bean, String beanName) 
            throws BeansException {
        return bean;
    }
    default Object postProcessAfterInitialization(Object bean, String beanName) 
            throws BeansException {
        return bean;
    }
}
复制代码

没错,Spring AOP就是通过BeanPostProcessor将目标对象替换为代理对象的!在Spring AOP中,这个BeanPostProcessor就是AbstractAutoProxyCreator抽象类,其主要用于创建代理对象。 Spring AOP为AbstractAutoProxyCreator定义了两个直系子类,分别是:BeanNameAutoProxyCreatorAbstractAdvisorAutoProxyCreator;前者根据Bean的名称来判断是否需要为当前Bean创建代理对象,后者根据Advisor探测结果来判断是否需要为该Bean创建代理对象;何为Advisor?Advisor是Spring AOP中独有的术语,在AspectJ中并没有等效的术语与其匹配,但其与切面还是有一定相似之处的,或者大家干脆将其视为一个特殊的切面,该切面只能包含一个Advice (通知) 和一个Pointcut (切入点) 而已;此外,Advisor有两个分支,分别是PointcutAdvisorIntroductionAdvisor

相较于BeanNameAutoProxyCreator,AbstractAdvisorAutoProxyCreator更为重要,AbstractAdvisorAutoProxyCreator有三个子类,分别是AspectJAwareAdvisorAutoProxyCreatorAnnotationAwareAspectJAutoProxyCreatorInfrastructureAdvisorAutoProxyCreator。一般,在Spring IoC中只会有一个名称为org.springframework.aop.config.internalAutoProxyCreator、类型为AbstractAdvisorAutoProxyCreator的Bean,如果classpath下没有Spring AOP依赖或者没有aspectjweaver依赖,那么Spring Boot会自动选用InfrastructureAdvisorAutoProxyCreator;否则将会选用AnnotationAwareAspectJAutoProxyCreator。大家可以通过下面这种方式来验证三者的优先级,AnnotationAwareAspectJAutoProxyCreator是优先级最高、最通用的一个。

public class AutoProxyCreatorPriorityApplication {
    public static final String AUTO_PROXY_CREATOR_BEAN_NAME =
            "org.springframework.aop.config.internalAutoProxyCreator";
    public static void main(String[] args) {
        // STEP 1:构造 BeanDefinitionRegistry
        // STEP 2 3 4 依次向 BeanDefinitionRegistry 中注册三种 AbstractAdvisorAutoProxyCreator BeanDefinition 实例,
        // 但 name 完全一致,即 AUTO_PROXY_CREATOR_BEAN_NAME
        BeanDefinitionRegistry beanDefinitionRegistry = new SimpleBeanDefinitionRegistry();

        // STEP 2:注册 InfrastructureAdvisorAutoProxyCreator BeanDefinition
        AopConfigUtils.registerAutoProxyCreatorIfNecessary(beanDefinitionRegistry);
        BeanDefinition infrastructureAdvisorCreatorBeanDefinition = beanDefinitionRegistry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
        System.out.println(infrastructureAdvisorCreatorBeanDefinition.getBeanClassName());

        // STEP 3:注册 AspectJAwareAdvisorAutoProxyCreator BeanDefinition
        AopConfigUtils.registerAspectJAutoProxyCreatorIfNecessary(beanDefinitionRegistry);
        BeanDefinition aspectJAwareAdvisorCreatorBeanDefinition = beanDefinitionRegistry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
        System.out.println(aspectJAwareAdvisorCreatorBeanDefinition.getBeanClassName());

        // STEP 4:注册 AnnotationAwareAspectJAutoProxyCreator BeanDefinition
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(beanDefinitionRegistry);
        BeanDefinition annotationAwareAspectJCreatorBeanDefinition = beanDefinitionRegistry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
        System.out.println(annotationAwareAspectJCreatorBeanDefinition.getBeanClassName());
    }
}
复制代码

InfrastructureAdvisorAutoProxyCreator是如何判断是否需要为当前Bean创建代理对象的呢?

  1. 首先,它会从Spring IoC容器中一次性获取所有Advisor;
  2. 然后,逐一遍历每个Advisor,若当前Advisor所对应的BeanDefinition的Role等于BeanDefinition.ROLE_INFRASTRUCTURE,那么该Advisor就具备候选资质;
  3. 最后,从具备候选资质的Advisor列表中选取与当前Bean匹配的Advisor,如果最终存在相匹配的Advisor,那么就为当前Bean创建代理对象;那么是如何裁定是否匹配的呢?若该Advisor是PointcutAdvisor类型,那么就根据ClassFilter与MethodMatcher去匹配当前Bean;若该Advisor是IntroductionAdvisor类型,那么就根据ClassFilter去匹配当前Bean。

AnnotationAwareAspectJAutoProxyCreator又是如何判断是否需要为当前Bean创建代理对象的呢?这部分逻辑比较复杂,如果想了解详细逻辑,参见笔者之前写的一篇文章《》。

  1. 首先,它会从Spring IoC容器中一次性获取所有Advisor (一般,这些Advisor是用户或者开源组件中自定义的),默认这些Advisor具备候选资质,压根不用像InfrastructureAdvisorAutoProxyCreator那样还要具体判断是否具备候选资质,这也从侧面说明:为什么AnnotationAwareAspectJAutoProxyCreator比InfrastructureAdvisorAutoProxyCreator优先级更高;
  2. 然后,它再从Spring IoC容器中获取所有由@Aspect标注的Bean,将这些切面Bean中由@Before、@After和@Around等标注的方法封装成一个PointcutAdvisor列表,至此将步骤一和步骤二中的Advisor组合为一个候选Advisor列表;
  3. 最后,从具备候选资质的Advisor列表中选取与当前Bean匹配的Advisor,如果最终存在相匹配的Advisor,那么就为当前Bean创建代理对象;那么是如何裁定是否匹配的呢?若该Advisor是PointcutAdvisor类型,那么就根据ClassFilter与MethodMatcher去匹配当前Bean;若该Advisor是IntroductionAdvisor类型,那么就根据ClassFilter去匹配当前Bean。

Spring AOP之所以能支持以Aspectj注解风格去定义切面,靠的就是AnnotationAwareAspectJAutoProxyCreator!

代理对象的创建规则已经清晰了,接下来就要搞清楚究竟是如何创建代理对象的。Spring AOP依托JDK动态代理CGLIB代理技术来创建代理对象,关于这方面的知识参见笔者之前写的一篇文章《Java动态代理》,这里就不再赘述了。

理论知识基本介绍完毕,下面进入实战环节。摆在大家面前的第一道坎应该是选取合适的Advisor,究竟是PointcutAdvisor还是IntroductionAdvisor呢?PointcutAdvisor持有一个Advice和一个Pointcut,Spring AOP 将Advice建模为org.aopalliance.intercept.MethodInterctptor拦截器,Pointcut用于声明应该在哪些Joinpoint (连接点) 处应用切面逻辑,而Joinpoint在SpringAOP 中专指方法的执行,因此,PointcutAdvisor中的Advice是方法级的拦截器;IntroductionAdvisor仅持有一个Advice和一个ClassFilter,显然,IntroductionAdvisor中的Advice是类级的拦截器。如果选用IntroductionAdvisor,可我们无法知道哪些类需要拦截啊,相反,如果选用PointcutAdvisor,那就可以借助MethodMatcher中的matches()方法准确拦截持有@OperationLog注解的目标方法。既然认准了PointcutAdvisor,那既可以直接实现PointcutAdvisor接口,也可以继承AbstractPointcutAdvisor,还可以继承AbstractBeanFactoryPointcutAdvisor,怎么搞都行,只要能包住Advice和Pointcut就行。如下所示:

public class OperationLogPointcutAdvisor extends AbstractBeanFactoryPointcutAdvisor {
    private Pointcut pointcut;

    public OperationLogPointcutAdvisor() {
    }

    /**
     * @param pointcut
     * @param advice
     */
    public OperationLogPointcutAdvisor(Pointcut pointcut, Advice advice) {
        this.pointcut = pointcut;
        setAdvice(advice);
    }

    public void setPointcut(Pointcut pointcut) {
        this.pointcut = pointcut;
    }

    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }
}
复制代码

Entonces, ¿cómo elegir Pointcut? Esperamos que su ClassFiler pueda coincidir con todas las clases, y su MethodMatcher solo necesita coincidir estáticamente, es decir, el isRuntime()método en MethodMatcher devuelve false, por lo que, en total, StaticMethodMatcherPointcutencaja perfectamente. Como sigue:

public class OperationLogPointcut extends StaticMethodMatcherPointcut {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        Annotation operationLogAnnotation = AnnotationUtils.findAnnotation(method, OperationLog.class);
        return Objects.nonNull(operationLogAnnotation);
    }
}
复制代码

Para obtener asesoramiento, puede usar directamente la potente interfaz org.aopalliance.intercept.MethodInterceptor, que puede simular la implementación MethodBeforeAdvice, AfterReturningAdviceetc. ThrowsAdviceComo sigue:

public class OperationLogAdvice implements MethodInterceptor {

    private OperatorService operatorService;

    private LogRecordPersistenceService logRecordPersistenceService;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object result = null;
        Throwable throwable = null;
        try {
            result = invocation.proceed();
        } catch (Throwable e) {
            throwable = e;
        }

        Method method = invocation.getMethod();
        Annotation operationLogAnnotation = AnnotationUtils.findAnnotation(method, OperationLog.class);
        Map<String, Object> operationLogAnnotationAttrMap = AnnotationUtils.getAnnotationAttributes(operationLogAnnotation);
        Operator operator = getOperator();
        BizCategory bizCategory =  (BizCategory) operationLogAnnotationAttrMap.get("bizCategory");
        String bizTarget =  (String) operationLogAnnotationAttrMap.get("bizTarget");
        String operation = String.format("%s %s %s", operator.getOperatorName(), bizCategory.getName(), bizTarget);
        LogRecord logRecord = encapsulateLogRecord(operator, bizTarget, operation, throwable);
        logRecordPersistenceService.doLogRecordPersistence(logRecord);

        if (Objects.nonNull(throwable)) {
            throw throwable;
        }

        return result;
    }
}
复制代码

Todo está listo, OperationLogPointcutAdvisordecláralo como un Bean, pero no te pierdas esta línea: @Role(BeanDefinition.ROLE_INFRASTRUCTURE). ¿Por qué es muy recomendable agregar esta línea de código? ¡Piénsalo!

bifurcame

https://github.com/dk900912/op…

Supongo que te gusta

Origin juejin.im/post/7098306656429146148
Recomendado
Clasificación