• Java自定义注解以及Spring的AOP详解,通过AOP和自定义注解实现日志记录


    一:自定义注解简介

    1.描述

    注解的使用真的很神奇,加一个注解就能实现想要的功能,很好奇,也想自己根据需要写一些自己实现的自定义注解。问题来了,自定义注解到底是什么?其实注解一点也不神奇,注解是一种能被添加到java源代码中的元数据,单独使用注解,就相当于在类、方法、参数和包上加上一个装饰,什么功能也没有,仅仅是一个标志,然后这个标志可以加上一些自己定义的参数。就像下面这样,创建一个@interface的注解,然后就可以使用这个注解了,加在我们需要装饰的方法上,但是什么功能也没有。

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface ValidateToken {
        String value() default"";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.基本介绍

    • 修饰符:访问修饰符必须为public,不写默认为pubic
    • 关键字:关键字为@interface
    • 注解名称:注解名称为自定义注解的名称,例如上面的XinLinLog 就是注解名称
    • 注解类型元素:注解类型元素是注解中内容,根据需要标志参数,例如上面的注解的value

    3.元注解(@Target、@Retention、@Inherited、@Documented)

    我们上面的创建的注解ValidateToken 上面还有几个注解(@Target、@Retention、@Inherited、@Documented),这四个注解就是元注解,元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的元注解类型,它们被用来提供对其它 注解类型作标志操作(可以理解为最小的注解,基础注解)

    1)@Target——用于描述注解的使用范围,该注解可以使用在什么地方

    Target类型描述
    ElementType.TYPE应用于类、接口(包括注解类型)、枚举
    ElementType.FIELD应用于属性(包括枚举中的常量)
    ElementType.METHOD应用于方法
    ElementType.PARAMETER应用于方法的形参
    ElementType.CONSTRUCTOR应用于构造函数
    ElementType.LOCAL_VARIABLE应用于局部变量
    ElementType.ANNOTATION_TYPE应用于注解类型
    ElementType.PACKAGE应用于包

    备注:例如@Target(ElementType.METHOD),标志的注解使用在方法上,但是我们在这个注解标志在类上,就会报错

    2)@Retention——表明该注解的生命周期

    生命周期类型描述
    RetentionPolicy.SOURCE编译时被丢弃,不包含在类文件中
    RetentionPolicy.CLASSJVM加载时被丢弃,包含在类文件中,默认值
    RetentionPolicy.RUNTIME由JVM 加载,包含在类文件中,在运行时可以被获取到

    3)@Inherited——是一个标记注解,其子类也可以使用该注解的功能

    @Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

    4)@Documented——表明该注解标记的元素可以被Javadoc 或类似的工具文档化

    二:Spring AOP详解

    1.前言

    1)什么是AOP

    AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面,是Spring的核心思想之一。

    2)AOP 实现分类

    AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码,按照 AOP 框架修改源代码的时机,可以将其分为两类:

    • 静态 AOP 实现, AOP 框架在编译阶段对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class文件已经被改掉了,需要使用特定的编译器),比如 AspectJ。
    • 动态 AOP 实现, AOP 框架在运行阶段对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP。

    3)AOP核心概念

    在这里插入图片描述

    • 切面(Aspect):切面是通知和切点的结合。
    • 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
    • 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
    • 目标对象(Target):目标对象指将要被增强的对象。
    • 切点(PointCut): 可以插入增强处理的连接点。
    • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
    • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。
    • 顾问(Advisor):顾问是Advice的一种包装体现,Advisor是Pointcut以及Advice的一个结合,用来管理Advice和Pointcut。

    4)AOP源码解析

    我们知道,spring中的aop是通过动态代理实现的,那么他具体是如何实现的呢?spring通过一个切面类,在他的类上加入@Aspect注解,定义一个Pointcut方法,最后定义一系列的增强方法。这样就完成一个对象的切面操作。
    那么思考一下,按照上述的基础,要实现我们的aop,大致有以下思路:
    1.找到所有的切面类
    2.解析出所有的advice并保存
    3.创建一个动态代理类
    4.调用被代理类的方法时,找到他的所有增强器,并增强当前的方法

    5)AOP在工作中的作用

    • 在调用service具体一些业务方法的时候,想在前面打一些日志。
    • 通过前后两次取时间戳来减一下,来统计所有业务方法执行的时间。
    • 在调用某一类业务方法时,判断用户有没有权限。
    • 在一系列业务方法前后加上事务的控制。
    • 比如startTransaction、commitTransaction(模拟事务控制)。

    6)JDK动态代理和CGLIB动态代理

    • jdk:假如目标对象(被代理对象)实现接口,则底层可以采用JDK动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
    • CGLIB:假如目标对象(被代理对象)没有实现接口,则底层可以采用CGLIB代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)。

    2.AOP 相关术语分析

    1)术语分析

    • 切面(aspect): 横切面对象,一般为一个具体类对象(可以借助@Aspect声明)。
    • 通知(Advice):在切面的某个特定连接点上执行的动作(扩展功能),例如around,before,after等。
    • 连接点(joinpoint):程序执行过程中某个特定的点,一般指被拦截到的的方法。
    • 切入点(pointcut):对多个连接点(Joinpoint)一种定义,一般可以理解为多个连接点的集合。
      在这里插入图片描述

    2)注解作用

    • @Aspect 注解用于标识或者描述AOP中的切面类型,基于切面类型构建的对象用于为目标对象进行功能扩展或控制目标对象的执行。
    • @Pointcut注解用于描述切面中的方法,并定义切面中的切入点(基于特定表达式的方式进行描述),在本案例中切入点表达式用的是bean表达式,这个表达式以bean开头,bean括号中的内容为一个spring管理的某个bean对象的名字。
    • @Around注解用于描述切面中方法,这样的方法会被认为是一个环绕通知(核心业务方法执行之前和之后要执行的一个动作),@Aournd注解内部value属性的值为一个切入点表达式或者是切入点表达式的一个引用(这个引用为一个@PointCut注解描述的方法的方法名)。
    • ProceedingJoinPoint类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息。一般用于@Around注解描述的方法参数。

    3.AOP编程增强

    1)通知类型

    • 前置通知 (@Before) 。
    • 返回通知 (@AfterReturning) 。
    • 异常通知 (@AfterThrowing) 。
    • 后置通知 (@After)。
    • 环绕通知 (@Around) :重点掌握(优先级最高)

    2)通知执行顺序

    在这里插入图片描述

    4.切入点表达式

    1)bean表达式

    bean表达式一般应用于类级别,实现粗粒度的切入点定义,案例分析:

    • bean(“userServiceImpl”)指定一个userServiceImpl类中所有方法。
    • bean(“*ServiceImpl”)指定所有后缀为ServiceImpl的类中所有方法。
      说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的名字应该是spring容器中某个bean的name。

    2)within表达式

    within表达式应用于类级别,实现粗粒度的切入点表达式定义,案例分析:

    • within(“aop.service.UserServiceImpl”)指定当前包中这个类内部的所有方法。
    • within(“aop.service.*”) 指定当前目录下的所有类的所有方法。
    • within(“aop.service…*”) 指定当前目录以及子目录中类的所有方法。

    3)execution表达式

    execution表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析:
    语法:execution(返回值类型 包名.类名.方法名(参数列表))。

    • execution(void aop.service.UserServiceImpl.addUser())匹配addUser方法。
    • execution(void aop.service.PersonServiceImpl.addUser(String)) 方法参数必须为String的addUser方法。
    • execution(* aop.service….(…)) 万能配置。

    4)@annotation表达式

    @annotaion表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析

    • @annotation(anno.RequiredLog) 匹配有此注解描述的方法。
    • @annotation(anno.RequiredCache) 匹配有此注解描述的方法。
      其中:RequiredLog为我们自己定义的注解,当我们使用@RequiredLog注解修饰业务层方法时,系统底层会在执行此方法时进行日扩展操作。

    三:自定义注解使用——通过自定义注解记录日志

    自定义注解使用范围:
    上面总结的注解的定义,但是创建这样一个注解,仅仅是一个标志,装饰类、方法、属性的,并没有功能,要想实现功能,需要我们通过拦截器、AOP切面这些地方获取注解标志,然后实现我们的功能。一般我们可以通过注解来实现一些重复的逻辑,就像封装了的一个方法,可以用在一些权限校验、字段校验、字段属性注入、保存日志、缓存。

    1.定义Pointcut切面

       /**
    	 * @Pointcut 注解通过切入点表达式定义切入点,例如
    	 * bean表达式:bean(bean的名称)
    	 */
    	//@Pointcut("bean(sysUserServiceImpl)")
    	@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
    	public void doLogPointCut() {
    		//方法体不需要写任何内容
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • @Pointcut:获取添加自定义注解的方法,获取某些特定的类
    • 方法体中不需要添加任何东西

    2.定义环绕通知

        @Around("doLogPointCut()")
        public Object around(ProceedingJoinPoint jp)throws Throwable{
    		long t1=System.currentTimeMillis();
    		log.info("start:{}",t1);//{}在这里表示占位符
    		try {
    		Object result=jp.proceed();//调用本类内部切入点对应其它通知或其它切面或目标方法。
    		long t2=System.currentTimeMillis();
    		log.info("end: {}",t2);
    		saveLog(jp,(t2-t1));//记录正常行为日志
    		return result;
    		}catch (Throwable e) {
    		log.error("error end: {}",e.getMessage());
    		//可以在此位置记录异常行为日志
    		//return null;
    		throw e;
    		}
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1) @Around 注解描述的方法为一个通知方法,在这个方法内部可以通过连接点对象(ProceedingJoinPoint)调用目标方法,并在目标方法对象执行之前或之后添加额外功能。

    2) @Around 注解描述的方法有一定要求:

    • 返回值类型为Object
    • 方法参数类型为ProceedingJoinPoint类型
    • 方法抛出throwable异常(建议)

    3)jp 封装了正要执行的目标方法信息

    4)Object result=jp.proceed();可以获取到目标方法执行的结果和时间。

    3.保存用户行为信息

    private void saveLog(ProceedingJoinPoint jp,long time) throws NoSuchMethodException, SecurityException, JsonProcessingException {
    		//1.获取用户行为信息
    		//1.1获取目标方法对象
    		Method targetMethod=getTargetMethod(jp);
    		//1.2获取目标方法的方法名信息
    		String targetMethodName=
    	    targetMethod.getDeclaringClass().getName()+"."+targetMethod.getName();
    		//1.3获取目标方法上的操作名
    		String operation = getOperation(targetMethod);
    		//1.4 目标方法参数(转换为字符串)
    		//String params=Arrays.toString(jp.getArgs());
    		String params=
    		//将参数对象转换为json格式字符串
    		new ObjectMapper().writeValueAsString(jp.getArgs());
    		//2.封装用户行为信息
    		SysLog log=new SysLog();
    		log.setIp(IPUtils.getIpAddr());
    		log.setUsername(ShiroUtils.getUsername());//后续做完登陆以后,为登陆用户名
    		log.setOperation(operation);
    		log.setMethod(targetMethodName);
    		log.setParams(params);
    		log.setTime(time);
    		log.setCreatedTime(new Date());
    		//3.保存用户行为信息
    		sysLogService.saveObject(log);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    4.获取目标方法

    private Method getTargetMethod(ProceedingJoinPoint jp) throws NoSuchMethodException, SecurityException {
    		Class<?> targetCls=jp.getTarget().getClass();
    		MethodSignature ms=(MethodSignature)jp.getSignature();
    		//获取目标方法
    		Method targetMethod=//目标方法(类全名+方法名)
    		targetCls.getDeclaredMethod(ms.getName(),ms.getParameterTypes());
    		return targetMethod;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    5.获取操作名

    	private String getOperation(Method targetMethod) {
    		String operation="operation";
    		RequiredLog requiredLog=
    		targetMethod.getAnnotation(RequiredLog.class);
    		if(requiredLog!=null) {
    		operation=requiredLog.value();
    		}
    		return operation;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    【JVM故障问题排查心得】「内存诊断系列」JVM内存与Kubernetes中pod的内存、容器的内存不一致所引发的OOMKilled问题总结(上)
    如何在Rocky Linux 8上安装LAMP栈
    POSIX与System v消息队列
    TMS FMX Cloud提供集成元素
    【项目方案】利用Zookeeper实现集群缓存一致
    React合成事件
    SpringBoot整合ElasticSearch
    国科大卜东波算法设计作业
    mac中文件夹怎么显示.git隐藏文件
    CompletableFuture 异步编排、案例及应用小案例
  • 原文地址:https://blog.csdn.net/suiyishiguang/article/details/126612592