Spring AOP+注解 实现完美日志记录

走远了吗. 提交于 2020-04-05 21:50:44

 

 Spring IOC 及其 AOP是其两大核心功能,本篇介绍下AOP的相关实际知识和实际应用。下面先简单介绍下aop的概念和基础使用。

一、基本介绍

AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。业务处理的代码完全不知道他会被代理,他也不清楚自己在执行前后会发生什么事情。核心关注点横切关注点 是完全不相干的两种的处理方式。

 

AOP核心概念

1、横切关注点

对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点

2、切面(aspect)

类是对物体特征的抽象,切面就是对横切关注点的抽象

3、连接点(joinpoint)

被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器

4、切入点(pointcut)

对连接点进行拦截的定义

5、通知(advice)

所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类

6、目标对象

代理的目标对象

7、织入(weave)

将切面应用到目标对象并导致代理对象创建的过程

8、引入(introduction)

在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

 

Spring对AOP的支持

Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为:

1、默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了

2、当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB

AOP编程其实是很简单的事情,纵观AOP编程,程序员只需要参与三个部分:

1、定义普通业务组件

2、定义切入点,一个切入点可能横切多个业务组件

3、定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作

所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。

 

二、实际使用

aop最常见的使用场景就是 认证、权限、日志、事务管理四大方向。当然,根据自己的业务需求可以实现很多功能。下面就聊聊我的实战代码。

需求:程序API的调用入口需要记录调用方的相关信息,传入参数,执行成功与否。

实现:常用的就是两种方式:

    一是 限定包名+方法名,比如: "execution(public * com.yss.shopping.controller..*.*(..))"

    二是 采用注解的方式。

分析:由于不同模块的不同方法都有可能需要记录相关日志,比如 controller或者service等,用于排查问题定位使用。而这些方法的包名也可能不一致,方法名称也取的不一致。所以采用包名+方法名就不够灵活,代码复用性不高,因此,我采用了注解的方式。

思路:开发一个注解类,定义相关属性,在有需要的日志记录的地方就加上相关注解。切入点则选取有注解类的方法,在业务方法执行前后进行相应日志记录。

 

注解类:

我定义了三个属性,module——真正的业务分为好多模块的,比如 支付,浏览记录、购物车、评论、优惠券等等。objectType——操作类型,比如 更新、创建、删除,手动、自动 等,description——描述,即一些通用的记录信息,比如可以加上时间、地点、成功或者失败等等。

其核心作用就是会将这些信息在advice里做真正的日志记录时,会将其丰富为相应的日志对象。

package com.log.api.entity;

import java.lang.annotation.*;

/**
 * 日志记录
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogRecord {
    String module() default ""; // 操作模块
    String objectType() default "";  // 操作类型
    String description() default "";  // 操作说明
}

下面就是真正的切面类,用于处理日志记录。

@Component 交给spring管理

@Aspect 这是一个切面类

@PointCut 标记切入点,也就是哪个方法需要被切入。

@AfterThrowing 抛异常时候将会执行该方法。

@After 业务方法执行完了将会执行该方法。

除此之外了,还有三种通知方式,@Around, @Before @ AfterReturning 可结合业务需求选择相应的通知方式。

重点说明

 1. 建议将通知里的代码 try catch ,确保切面类不会抛出异常,以免影响正常的业务逻辑。这个坑已经踩过了。

2. 注意包名一定要被spring 扫描到,如果你的切面类没有发生作用,首先检查包名,其次查看切入点的表达式是否正确。

/**
 * 日志记录
 */
@Component
@Aspect
@Slf4j
public class LogAspect {
    @Autowired
    protected ILogService logService;

    @Pointcut("@annotation(com.log.api.entity.LogRecord)")
    public void pointcut() {
    }

    @AfterThrowing(value = "pointcut()", throwing = "e")
    public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
        log.warn("object sync error ",e);
        try {

            LogBrief logBrief = enrichLog(joinPoint);
            logBrief.setExecuteMessage(e.getMessage()+" ## "+logBrief.getExecuteMessage());
            logService.addLog(logBrief);
        }catch (Exception e1){
            log.error("aop record log error,{}",e);
        }
    }
    /**
     * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
     *
     * @param joinPoint 切入点
     */
    @After("pointcut()")
    public void saveNormalLog(JoinPoint joinPoint) {
        try{
            LogBrief logBrief = enrichLog(joinPoint);
            logBrief.setExecuteStatus("SUCCESS");
            //保存日志
            logService.addLog(logBrief);
        }catch (Exception e){
            log.error("aop record log error,{}",e);
        }
    }
    public LogBrief enrichLog(JoinPoint joinPoint){
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogRecord logAnnotation = method.getAnnotation(LogRecord.class);
        LogBrief logBrief = buildLogBrief();
        if (logAnnotation == null) {
            return logBrief;
        }
        logBrief.setModule(logAnnotation.module());
        logBrief.setObjectType(logAnnotation.objectType());
        // 请求的方法参数值
        Object[] args = joinPoint.getArgs();
        // 请求的类名
        String className = joinPoint.getTarget().getClass().getName();

        // 请求的方法参数名称
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames = u.getParameterNames(method);
        String description = logAnnotation.description();
        if (args != null) {
            for (int i = 0; i < args.length; i++) {
                description+= JSON.toJSONString(args[i] )+"==";
            }
        }
        logBrief.setExecuteMessage(description);
        return logBrief;
    }

代码中的LogBrief对象,请根据自身需要定义。以上便是自己使用aop+注解解决实际需求的开发过程。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!