美文网首页
自定义注解实现打印系统日志

自定义注解实现打印系统日志

作者: 小疏林er | 来源:发表于2021-11-09 20:26 被阅读0次

    1 注解(Annotation)

    • Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
    • Java 语言中的类、方法、变量、参数和包等都可以被标注。注解通过反射来在运行时获取标注内容(在编译器生成类文件时,标注可以被嵌入到字节码中,Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容。)

    1.1 常见注解

    • Java内置注解
      • java.lang包下
        • @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
        • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
        • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
        • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
        • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口
      • java.lang.annotation包下(元注解:标注注解的注解)
        • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
        • @Documented - 标记这些注解是否包含在用户文档中。
        • @Target - 标记这个注解应该是哪种 Java 成员。
        • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
        • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
    • 自定义注解

    1.2 自定义注解

    1.2.1 注解声明方式

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface SysLog {
        //注解的参数 参数类型+参数名+();
        //default 代表默认值, 正常注解有参数不传会报错,如果提供了默认值则不会报错
        //默认值为-1代表不存在找不到
        //参数名为value时 使用注解时可以不用写 value=XXX
    
        /**
         * All: 全部通知都执行
         * BEFORE: 前置通知,主要打印入参
         * AFTER: 后置通知
         * NO_THROW: 异常通知,只有数组里面有NO_THROW时,发生异常时才不会通知,其他情况,发生异常一律通知
         * AROUND: 环绕通知,主要打印方法执行时间
         * RETURN: 返回通知:主要打印执行结果
         */
        String[] value() default {"BEFORE","AROUND","RETURN"};
    }
    

    1.2.2 常用元注解使用说明

    • @Target:指明了修饰的这个注解的使用范围,即被描述的注解可以用在哪里。

      • TYPE:类,接口或者枚举
      • FIELD:域,包含枚举常量
      • METHOD:方法
      • PARAMETER:参数
      • CONSTRUCTOR:构造方法
      • LOCAL_VARIABLE:局部变量
      • ANNOTATION_TYPE:注解类型
      • PACKAGE:包
    • @Retention:指明修饰的注解的生存周期,即会保留到哪个阶段。(RUNTIME>CLASS>SOURCE)

      • SOURCE:源码级别保留,编译后即丢弃
      • CLASS:编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值
      • RUNTIME: 运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用
    • @Documented:指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。

    • @Inherited 表明子类可以继承父类注解

    2 AOP 面向切面编程

    2.1 基本概念

    在程序运行过程中,动态的将代码插入到原有的指定方法、指定位置上的思想被称之为面向切面编程。

    • advice: 增强、通知,在特定连接点执行的动作(其实就是你要插入到的代码)。
    • pointcut: 切点,一组连接点的总称,用于指定某个增强应该在何时被调用(像是对一组连接点的抽象声明,通常是execution表达式,即符合表达式的方法被称为切点)。
    • joinpoint: 连接点,在应用执行过程中能够插入切面的一个点。
    • aspect: 切面,即通知(增强)和切点的结合。

    2.2 aspectj实现面向切面编程

    2.2.1 定义切面

    • 切面是切点和增强的结合,使用aspectj定义切面时,需要准备一个类,来定义各种通知方法及实现。类上需要加上@Aspect注解。

    2.2.2 定义切点

    定义一个切点需要两部分组成:Pointcut表示式和Point签名。
    Pointcut表示式通常有两种类型,一种是execution表达式,一种就是注解。
    @Around("logPointCut()") 等价于@Around("@annotation(com.sler.springcloud.utils.SysLog)")

    例:execution表达式:public * com.sler.springcloud.controller...(..)
    public 代表访问权限
    * 代表任意返回值
    controller.. 代表当前包及子包
    * 代表所有类
    .*(..) 代表类下面所有方法,(..)代表允许任何形式的入参

        @Pointcut("@annotation(com.sler.springcloud.utils.SysLog)")  //Pointcut表示式
        public void logPointCut() {}                                 //Point签名
    

    2.2.3 五种通知(增强)

    • @Before: 前置通知, 在方法执行之前执行
    • @After: 后置通知, 在方法执行之后执行 。后置方法在连接点方法完成之后执行,无论连接点方法执行成功还是出现异常,都将执行后置方法。
    • @AfterRunning: 返回通知, 在方法返回结果之后执行 当连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行。
    • @AfterThrowing: 异常通知, 在方法抛出异常之后 异常通知方法只在连接点方法出现异常后才会执行,否则不执行。
    • @Around: 环绕通知, 围绕着方法执行
    • 执行顺序 环绕前 -> 前置 -> 环绕后(异常时无) -> 后置 -> 返回通知(或异常通知,二者存其一)

    3 开发系统日志注解

    3.1 代码实现

    3.1.1 自定义注解类

    package com.sler.springcloud.utils;
    
    import java.lang.annotation.*;
    
    /**
     * 类功能:系统日志注解
     * 作者: sler
     * 创建时间: 2021/10/20 18:38
     * 描述:元注解 :@Target @Retention @Documented @Inherited
     */
    
    /** Target:指明了修饰的这个注解的使用范围,即被描述的注解可以用在哪里。
     *
     * TYPE:类,接口或者枚举
     * FIELD:域,包含枚举常量
     * METHOD:方法
     * PARAMETER:参数
     * CONSTRUCTOR:构造方法
     * LOCAL_VARIABLE:局部变量
     * ANNOTATION_TYPE:注解类型
     * PACKAGE:包
     */
    @Target(ElementType.METHOD)
    
    /** Retention:指明修饰的注解的生存周期,即会保留到哪个阶段。 RUNTIME>CLASS>SOURCE
     *
     * SOURCE:源码级别保留,编译后即丢弃
     * CLASS:编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值
     * RUNTIME: 运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用
     */
    @Retention(RetentionPolicy.RUNTIME)
    
    /**
     * Documented:指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。
     */
    @Documented
    
    /**
     * 子类可以继承父类注解
     */
    @Inherited
    public @interface SysLog {
        //注解的参数 参数类型+参数名+();
        //default 代表默认值, 正常注解有参数不传会报错,如果提供了默认值则不会报错
        //默认值为-1代表不存在找不到
        //参数名为value时 使用注解时可以不用写 value=XXX
    
        /**
         * All: 全部通知都执行
         * BEFORE: 前置通知,主要打印入参
         * AFTER: 后置通知
         * NO_THROW: 异常通知,只有数组里面有NO_THROW时,发生异常时才不会通知,其他情况,发生异常一律通知
         * AROUND: 环绕通知,主要打印方法执行时间
         * RETURN: 返回通知:主要打印执行结果
         */
        String[] value() default {"BEFORE","AROUND","RETURN"};
    }
    

    3.1.2 切面类

    package com.sler.springcloud.utils;
    
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * 类功能:系统日志切面
     * 作者: sler
     * 创建时间: 2021/10/20 18:41
     * 描述:
     */
    
    /**
     * @Before: 前置通知, 在方法执行之前执行
     * @After: 后置通知, 在方法执行之后执行 。后置方法在连接点方法完成之后执行,无论连接点方法执行成功还是出现异常,都将执行后置方法。
     * @AfterRunning: 返回通知, 在方法返回结果之后执行 当连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行。
     * @AfterThrowing: 异常通知, 在方法抛出异常之后 异常通知方法只在连接点方法出现异常后才会执行,否则不执行。
     * @Around: 环绕通知, 围绕着方法执行
     * <p>
     * 正常执行顺序 环绕前 -> 前置 -> 环绕后(异常时无) -> 后置 -> 返回通知(或异常通知,二者存其一)
     */
    
    @Aspect
    @Component
    @Slf4j
    public class SysLogAspect {
    
        /**
         * 定义切点
         * Pointcut切点包括
         *      Pointcut表示式:@Pointcut("@annotation(com.sler.springcloud.utils.SysLog)")
         *      Point签名:public void logPointCut(){}
         * 对定义好的切点进行增强时,可以使用表达式也可以使用签名
         */
        @Pointcut("@annotation(com.sler.springcloud.utils.SysLog)")
        public void logPointCut() {}
    
        /**
         * 环绕通知
         * @param joinPoint
         * @return
         * @throws Throwable
         */
    //    @Around("logPointCut()")
        @Around("@annotation(com.sler.springcloud.utils.SysLog)")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            //获取切点方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            //获取自定义注解
            SysLog sysLog = method.getAnnotation(SysLog.class);
            //获取注解值
            String[] value = sysLog.value();
            Object result = null;
    
            if (this.check("AROUND",value)){
                long beginTime = System.currentTimeMillis();    //开始计时
                //执行方法
                result = joinPoint.proceed();
                long time = System.currentTimeMillis() - beginTime; //执行时长
                String className = joinPoint.getTarget().getClass().getName();
                String methodName = signature.getName();
                log.info("系统环绕通知:方法" + className + "." + methodName + "(),执行时间:" + time + "ms");
                return result;
            }else {
                result = joinPoint.proceed();
                return result;
            }
        }
    
        /**
         * 异常日志 (可以做统一返回处理)
         *
         * @param joinPoint
         * @param e
         */
        @AfterThrowing(value = "logPointCut()", throwing = "e")
        public void afterThrowing(JoinPoint joinPoint, Exception e) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            //获取自定义注解
            SysLog sysLog = method.getAnnotation(SysLog.class);
            //获取注解值
            String[] value = sysLog.value();
            if (!this.check("NO_THROW",value)){
                String className = joinPoint.getTarget().getClass().getName();
                String methodName = signature.getName();
    
                log.error("系统异常通知:执行" + className + "." + methodName + "()方法发生异常,异常信息:" + e.toString());
            }
        }
    
        /**
         * 前置通知
         *
         * @param point
         */
        @Before("logPointCut()")
        public void beforMethod(JoinPoint point) {
            //获取切点方法
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            //获取自定义注解
            SysLog sysLog = method.getAnnotation(SysLog.class);
            //获取注解值
            String[] value = sysLog.value();
            if (this.check("BEFORE",value)){
                String className = point.getTarget().getClass().getName();
                String methodName = point.getSignature().getName();
                List<Object> args = Arrays.asList(point.getArgs());
    
                log.info("系统前置通知:待执行方法" + className + "." + methodName + "(),入参:" + args);
            }
        }
    
        /**
         * 后置通知
         *
         * @param point
         */
        @After("logPointCut()")
        public void afterMethod(JoinPoint point) {
            //获取切点方法
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            //获取自定义注解
            SysLog sysLog = method.getAnnotation(SysLog.class);
            //获取注解值
            String[] value = sysLog.value();
            if (this.check("AFTER",value)){
                String className = point.getTarget().getClass().getName();
                String methodName = point.getSignature().getName();
    
                log.info("系统后置通知:已执行方法" + className + "." + methodName+"()");
            }
        }
    
        /*通过returning属性指定连接点方法返回的结果放置在result变量中,在返回通知方法中可以从result变量中获取连接点方法的返回结果了。*/
    
        /**
         * 返回通知
         * @param point
         * @param result
         */
        @AfterReturning(value = "logPointCut()", returning = "result")
        public void afterReturning(JoinPoint point, Object result) {
            //获取切点方法
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            //获取自定义注解
            SysLog sysLog = method.getAnnotation(SysLog.class);
            //获取注解值
            String[] value = sysLog.value();
            if (this.check("RETURN",value)){
                String className = point.getTarget().getClass().getName();
                String methodName = point.getSignature().getName();
    
                log.info("系统返回通知:已执行方法" + className + "." + methodName + "(),执行结果:" + result);
            }
        }
    
        private boolean check(String type,String info[]){
            boolean flag = false;
            for (String s : info) {
                if("ALL".equals(s) || type.equals(s)){
                    return true;
                }
            }
            return flag;
        }
    
    }
    

    3.1.3 测试方法

        @SysLog
        @RequestMapping("/phone/{length}")
        public Object createData( @PathVariable int length) throws Exception {
            List<String> name = phoneUtils.getPhones(length);
            Object o = JSONArray.toJSON(name);
            return o;
        }
    

    3.2 测试结果

    执行结果.png

    相关文章

      网友评论

          本文标题:自定义注解实现打印系统日志

          本文链接:https://www.haomeiwen.com/subject/ywwsaltx.html