Si tiene alguna pregunta durante el uso, busque la cuenta pública de WeChat: ¡Du Xiaotou , el programador, y contácteme! ! !
"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定义了两个直系子类,分别是:BeanNameAutoProxyCreator
和AbstractAdvisorAutoProxyCreator
;前者根据Bean的名称来判断是否需要为当前Bean创建代理对象,后者根据Advisor探测结果来判断是否需要为该Bean创建代理对象;何为Advisor?Advisor是Spring AOP中独有的术语,在AspectJ中并没有等效的术语与其匹配,但其与切面还是有一定相似之处的,或者大家干脆将其视为一个特殊的切面,该切面只能包含一个Advice (通知) 和一个Pointcut (切入点) 而已;此外,Advisor有两个分支,分别是PointcutAdvisor
和IntroductionAdvisor
。
相较于BeanNameAutoProxyCreator,AbstractAdvisorAutoProxyCreator
更为重要,AbstractAdvisorAutoProxyCreator有三个子类,分别是AspectJAwareAdvisorAutoProxyCreator
、AnnotationAwareAspectJAutoProxyCreator
和InfrastructureAdvisorAutoProxyCreator
。一般,在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创建代理对象的呢?
- 首先,它会从Spring IoC容器中一次性获取所有Advisor;
- 然后,逐一遍历每个Advisor,若当前Advisor所对应的BeanDefinition的Role等于
BeanDefinition.ROLE_INFRASTRUCTURE
,那么该Advisor就具备候选资质; - 最后,从具备候选资质的Advisor列表中选取与当前Bean匹配的Advisor,如果最终存在相匹配的Advisor,那么就为当前Bean创建代理对象;那么是如何裁定是否匹配的呢?若该Advisor是PointcutAdvisor类型,那么就根据ClassFilter与MethodMatcher去匹配当前Bean;若该Advisor是IntroductionAdvisor类型,那么就根据ClassFilter去匹配当前Bean。
AnnotationAwareAspectJAutoProxyCreator
又是如何判断是否需要为当前Bean创建代理对象的呢?这部分逻辑比较复杂,如果想了解详细逻辑,参见笔者之前写的一篇文章《》。
- 首先,它会从Spring IoC容器中一次性获取所有Advisor (一般,这些Advisor是用户或者开源组件中自定义的),默认这些Advisor具备候选资质,压根不用像InfrastructureAdvisorAutoProxyCreator那样还要具体判断是否具备候选资质,这也从侧面说明:为什么AnnotationAwareAspectJAutoProxyCreator比InfrastructureAdvisorAutoProxyCreator优先级更高;
- 然后,它再从Spring IoC容器中获取所有由
@Aspect
标注的Bean,将这些切面Bean中由@Before、@After和@Around等标注的方法封装成一个PointcutAdvisor列表,至此将步骤一和步骤二中的Advisor组合为一个候选Advisor列表; - 最后,从具备候选资质的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, StaticMethodMatcherPointcut
encaja 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
, AfterReturningAdvice
etc. ThrowsAdvice
Como 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, OperationLogPointcutAdvisor
declá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!