侧边栏壁纸
博主头像
小明锅博主等级

没错,我就是小明,不过已经长大了,成为一名码农,在搬砖的同时,喜欢分享Java的编程知识,本网站致力于一站式后端人员开发,解决码农日常问题,挤出更多moyu时间

  • 累计撰写 15 篇文章
  • 累计创建 7 个标签
  • 累计收到 2 条评论
标签搜索

目 录CONTENT

文章目录

Springboot整合AOP和注解实现日志记录

小明锅
2024-04-15 / 0 评论 / 0 点赞 / 62 阅读 / 6,505 字 / 正在检测是否收录...

前言

​ 作为java开发工程师,日常curd工作少不了,特别是后台系统的操作,对于每一项操作我们都要记录,所以就得有操作日志,操作日志能够排除是开发的锅,是运营或者产品自己操作的。那么就有个问题,每次在业务处理最后,调用操作日志服务保存响应的日志,但是这段代码是很冗余的。

​ 其实想要解决这个问题,方法很多,可以用工厂模式,日志模板,日志切面等等。本讲就是利用Spring的AOP面向切面编程来实现日志操作,Sping官网也说了AOP主要用于日志,事务等处理,同时,自定义一个日志注解,那些业务操作需要记录日志,只要添加对应的注解就行,使得业务代码和日志代码低耦合。

一、AOP面向切面编程

​ AOP(Aspect Oriented Programming,面向切面编程),可以说是 OOP(Object Oriented Programing,面向对象编程)的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用来模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP 允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如权限管理、异常处理等也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。

​ 而 AOP 技术则恰恰相反,它利用一种称为 “横切” 的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为 “Aspect” ,即切面。**所谓“切面”,简单地说,就是将权限、事务、日志、异常等与业务逻辑相对独立的功能抽取封装,便于减少系统的重复代码,降低模块间的耦合度,增加代码的可维护性。**AOP 代表的是一个横向的关系,如果说 “对象” 是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向切面编程,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息,然后又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。

​ 所以,AOP可以使用如下场景:

  • 权限控制
  • 日志存储
  • 统一异常处理
  • 缓存处理
  • 事务处理

1.常用注解

​ AOP底层是用动态代理实现,依托的是采用的是JDK/CGLIB动态代理。不理解代理可以这一篇文章:代理模式

AOP常用注解:

@Pointcut :切入点,可以指定那个包/那个类,进行扫描

@Before :前置通知,在目标方法调用前执行通知

@After :后置通知,在目标方法完成(不管是抛出异常还是执行成功)后执行通知

@Around :环绕通知,在目标方法调用前后均可执行自定义逻辑

@AfterThrowing :异常通知,出现异常被执行

@AfterReturning:返回通知,在目标方法执行成功后,调用通知

2.AOP原理

​ 定义AOP切面Aspect,在spring容器初始化的时候,底层会通过动态代理的方式生成代理对象,当程序调用方法是在切入点内Pointcut,会调用到生成的字节码文件中,直接会找到DynamicAdvisoredIntercetor类的intercept方法,根据定好的通知器(Before/After/Around)会先生成一个拦截器链,依次执行,这一例操作都是异步的。

二、SpringBoot整合AOP

​ 废话不多说,接下来我们通过代码案例,整合AOP,本次主要依托springboot工程,

导入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

创建切面类:@Aspect标记为切面,开启AOP

@Component
@Aspect
public class LogAspect {

}

实现对应的通知器

切入点:* com.qm.springbootlogaspect.service.impl…*(…)) 即扫描service.impl这个包的所有类

@Component
@Aspect
public class LogAspect {

    private Logger logger = LoggerFactory.getLogger(LogAspect.class);

    /**
     * 配置aop切点,即扫描impl包下面的类和包是否加log注解
     */
    @Pointcut("execution(* com.qm.springbootlogaspect.service.impl..*(..))")
    private void pointcut() {
    }

    @Before(value = "pointcut()")
    public void saveUserOperate(JoinPoint joinPoint)throws NotFoundException, ClassNotFoundException  {
        System.out.println("开始新增操作日志------前置操作");

    }
    @After(value = "pointcut()")
    public void After(JoinPoint joinPoint) throws NotFoundException, ClassNotFoundException {
 
            System.out.println("新增操作日志结束"+user.getId()+"保存数据库------后置操作");
        
    }

测试 :启动springboot工程,控制器调用UserServiceImpl,可以发现输出上面前置后置通知器的内容。

image-1713164087943

这样我们就可以对所有业务操作之后,自动进行日志操作保存了

三、自定义日志注解

​ 上面通过整合AOP,可以已经实现了日志操作采集,但是是有没有发现。在切入点扫描下的所有包,都会记录日志,虽然控制了只扫描某个包,但是如果业务类多的话,对程序性能会有一定的影响。另一个问题是,我们没发知道是谁操作了,aop的通知器无法获取到操作用户的信息,也是业务类的信息。

​ 基于这个问题,我们想到可以注解作为载体,在切面点扫描的包,只有业务方法标记了自定义注解,才会保存操作日志,同时可以把业务信息(用户信息)通过注解传递给切面。

1.自定义日志注解

Java自定义注解很简单,注解中有个属性logStr,这个可以日志信息传递到切面。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Inherited
@Documented
public @interface Log {

    String logStr() default "";

}

2.定义注解解析类

新建日志工具类,用于日志保存的调用

public class LogUtils {

    private static LogUtils mInstance;

    private LogUtils() {
    }

    public static LogUtils get() {
        if (mInstance == null) {
            synchronized (LogUtils.class) {
                if (mInstance == null) {
                    mInstance = new LogUtils();
                }
            }
        }
        return mInstance;
    }

    public void setLog(String logStr) throws NotFoundException {
        String className = Thread.currentThread().getStackTrace()[2].getClassName();
        String methodName = Thread.currentThread().getStackTrace()[2].getMethodName();
        AnnotationUtils.get().setAnnotatioinFieldValue(className, methodName, Log.class.getName(), "logStr", logStr);
    }
}

新建注解内容解析类AnnotationUtils,主要可以修改注解上的属性值,获取注解中的属性值

public class AnnotationUtils {

    private static AnnotationUtils mInstance;

    public AnnotationUtils() {
    }

    public static AnnotationUtils get() {
        if (mInstance == null) {
            synchronized (AnnotationUtils.class) {
                if (mInstance == null) {
                    mInstance = new AnnotationUtils();
                }
            }
        }
        return mInstance;
    }

    /**
     * 修改注解上的属性值
     *
     * @param className  当前类名
     * @param methodName 当前方法名
     * @param annoName   方法上的注解名
     * @param fieldName  注解中的属性名
     * @param fieldValue 注解中的属性值
     * @throws NotFoundException
     */
    public void setAnnotatioinFieldValue(String className, String methodName, String annoName, String fieldName, String fieldValue) throws NotFoundException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ct = classPool.get(className);
        CtMethod ctMethod = ct.getDeclaredMethod(methodName);
        MethodInfo methodInfo = ctMethod.getMethodInfo();
        ConstPool constPool = methodInfo.getConstPool();
        AnnotationsAttribute attr = (AnnotationsAttribute) methodInfo.getAttribute(AnnotationsAttribute.visibleTag);
        Annotation annotation = attr.getAnnotation(annoName);
        if (annotation != null) {
            annotation.addMemberValue(fieldName, new StringMemberValue(fieldValue, constPool));
            attr.setAnnotation(annotation);
            methodInfo.addAttribute(attr);
        }
    }

    /**
     * 获取注解中的属性值
     *
     * @param className  当前类名
     * @param methodName 当前方法名
     * @param annoName   方法上的注解名
     * @param fieldName  注解中的属性名
     * @return
     * @throws NotFoundException
     */
    public String getAnnotatioinFieldValue(String className, String methodName, String annoName, String fieldName) throws NotFoundException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ct = classPool.get(className);
        CtMethod ctMethod = ct.getDeclaredMethod(methodName);
        MethodInfo methodInfo = ctMethod.getMethodInfo();
        AnnotationsAttribute attr = (AnnotationsAttribute) methodInfo.getAttribute(AnnotationsAttribute.visibleTag);
        String value = "";
        if (attr != null) {
            Annotation an = attr.getAnnotation(annoName);
            if (an != null)
                value = ((StringMemberValue) an.getMemberValue(fieldName)).getValue();
        }
        return value;
    }

}

3.测试

​ 在使用中,我们只要在需要日志保存的业务标记,@Log注解,如果需要保存对于业务信息,可以用LogUtils,把业务信息放入Log注解的字段中。如UserServiceImpl类:

    @Log
    public String findUserName(String tel) {

        System.out.println("tel:" + tel);
        User user = new User();
        user.setId("1").setName("aaa").setCreateTime(new Date());
        try {
            LogUtils.get().setLog(JSON.toJSONString(user));
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        return "zhangsan";
    }

切面后置处理器,增加日志注解判断,只有加了 @Log注解才会进行保存操作

 @After(value = "pointcut()")
    public void After(JoinPoint joinPoint) throws NotFoundException, ClassNotFoundException {
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();

        String logStr = AnnotationUtils.get().getAnnotatioinFieldValue(className, methodName, Log.class.getName(), "logStr");
        if (!StringUtils.isEmpty(logStr)) {
            // 获取json对象
            JSONObject jsonObject  = JSONObject.parseObject(logStr);
            User user = JSON.toJavaObject(jsonObject,User.class);
            logger.info("获取日志11:" + user);
            // 数据库记录操作...
            // 可以异步操作
            System.out.println("新增操作日志结束"+user.getId()+"保存数据库------后置操作");
        }
    }

接口调用测试,可以看到调用接口findUserName,控制台输出对应的切面日志保存信息

image-1713164109531

总结

​ 本文讲解什么是AOP,介绍AOP的常用注解,并且整合AOP实现日志记录,同时分析AOP记录日志的不足,加入自定义注解,更加细化精确的记录日志。本次日志记录的实现方案,也是我在项目中实际使用的,大家如果想要本文其他源码,可以订阅后私信哦。

0

评论区