美文网首页
面向切面编程AOP

面向切面编程AOP

作者: 此处应该有一个名字 | 来源:发表于2018-11-23 10:04 被阅读0次

最近开发中有一个需求,要求对指定的操作进行日志管理,以备以后查验。为了在保全大部分代码结构的前提下,选择了面向切面编程AOP,同时并不是全部的操作都需要进行日志管理,所以选择了自定义注解。

先简单介绍一下代码情况,在不破坏之前开发的代码结构的情况下,在公共工程中开发了注解和切面工程的大部分代码,在需要使用的工程中只进行配置的部分,根据业务需要还进行一个小小的调整,暂且不表。

第一个部分是定义一个注解,代码如下:

import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FcoTest {   
    String name() default "null";
}

首先@Retention: 定义注解的保留策略

  @Retention(RetentionPolicy.SOURCE)    // 注解仅存在于源码中,在class字节码文件中不包含

  @Retention(RetentionPolicy.CLASS)       // 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得

  @Retention(RetentionPolicy.RUNTIME)  // 注解会在class字节码文件中存在,在运行时可以通过反射获取到

三者的区别在于注解的作用时间,生命周期的长短,从上到下而言,越靠下生命周期越长。如果需要在运行时动态获取注解信息的话,只能用RUNTIME,而当如果需要在编译时就进行一些预处理的话,就需要选择CLASS,而只是一些检查性操作的话选取SOURCE会比较好一点。

而本次开发中需要深入业务层,所以还是选择了RUNTIME比较合适。

其次@Target 控制了触发注解的类型(定义注解的作用目标)

  @Target(ElementType.TYPE)                           //接口、类、枚举、注解

  @Target(ElementType.FIELD)                          //字段、枚举的常量

  @Target(ElementType.METHOD)                     //方法

  @Target(ElementType.PARAMETER)              //方法参数

  @Target(ElementType.CONSTRUCTOR)        //构造函数

  @Target(ElementType.LOCAL_VARIABLE)     //局部变量

  @Target(ElementType.ANNOTATION_TYPE)  //注解

  @Target(ElementType.PACKAGE)                   //包 

根据需要使用注解的位置使用合适的类型,本次开发主要是用在server层的方法上所以选择了@Target(ElementType.METHOD)。

最后@Document,它说明该注解将被包含在javadoc中。
PS:不知道为什么要加,但是加了也没坏处,就加上了。


第二个部分是定义了一个切面,切入点以及处理操作,代码如下:

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.fco.auth.front.utils.Dql;
import org.fco.auth.front.utils.IdWorkerUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Aspect
@Component
public class LogAspect {
    @Pointcut("@annotation(org.fco.auth.front.annotation.FcoTest)")
    public void annotationPointCut(){}
    @Before("annotationPointCut()")
    public void before (JoinPoint joinPoint) {
        MethodSignature sign =  (MethodSignature)joinPoint.getSignature();
        Method method = sign.getMethod();
        log.error("the opt id is {}", LogContext.getOperateId());
        System.out.println("函数名:" + method.getName());
        Object[] params = joinPoint.getArgs();
        String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
        Map map = new HashMap<String, Object>();
        for (int i = 0; i < names.length; i++) {
            map.put(names[i], params[i]);
        }
        log.error("the params  is {}", JSON.toJSONString(map));
        // 存库
        Dql.fco().insert("saveLog").params(IdWorkerUtils.next(), method.getName(),         JSON.toJSONString(map), LogContext.getOperateId()).execute();
    }
}

首先@Aspect作用是把当前类标识为一个切面供容器读取

  @Component是把普通pojo实例化到spring容器中的一个注解,其实就是相当于让Spring把这个类纳入管理之中。

其次@Pointcut:声明一个切入点,切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。SpringAOP只支持Springbean的方法执行连接点。所以你可以把切入点看做是Spring bean上方法执行的匹配。一个切入点声明有两个部分:一个包含名字和任意参数的签名,还有一个切入点表达式,该表达式决定了我们关注那个方法的执行。注:作为切入点签名的方法必须返回void类型。此处使用的是@annotation的方式。

Spring AOP支持在切入点表达式中使用如下的切入点指示符:

      execution - 匹配方法执行的连接点,这是你将会用到的Spring的最主要的切入点指示符。(用的最多)

      within - 限定匹配特定类型的连接点(在使用Spring AOP的时候,在匹配的类型中定义的方法的执行)。

      this - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中bean reference(Spring AOP 代理)是指定类型的实例。

      target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中目标对象(被代理的应用对象)是指定类型的实例。

      args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中参数是指定类型的实例。

      @target - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中正执行对象的类持有指定类型的注解。

      @args - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中实际传入参数的运行时类型持有指定类型的注解。

      @within - 限定匹配特定的连接点,其中连接点所在类型已指定注解(在使用Spring AOP的时候,所执行的方法所在类型已指定注解)。

      @annotation - 限定匹配特定的连接点(使用Spring AOP的时候方法的执行),其中连接点的主题持有指定的注解。

      execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

      modifier-pattern:表示方法的修饰符

      ret-type-pattern:表示方法的返回值

      declaring-type-pattern?:表示方法所在的类的路径

      name-pattern:表示方法名

      param-pattern:表示方法的参数

      throws-pattern:表示方法抛出的异常

      其中后面跟着“?”的是可选项。*可以匹配任何值,可以用(..)表示零个或多个任意的方法参数,使用&&符号表示与关系,使用||表示或关系、使用!表示非关系。在XML文件中使用and、or和not这三个符号。

      例如:excecution(* com.tianmaying.service.BlogService.updateBlog(..))

      1)execution(* *(..))  //表示匹配所有方法 

      2)execution(public * org.fco.admin.server.UserService.*(..))  //表示匹配org.fco.admin.server.UserService中所有的公有方法 

      3)execution(* org.fco.admin.server.*.*(..))  //org.fco.admin.server包及其子包下的所有方法

      @annotation(注解的地址)

      例如:"@annotation(org.fco.auth.front.annotation.FcoTest)"

      除了@annotation和@args外,还有另外两个用于注解的切点函数,分别是@target和@within和@annotation @args函数一样,@target和@within也只接受注解类名作为入参。其中@target(M)匹配任意标注了@M的目标类,而@within(M)匹配标注了@M的类及其子孙类

      @target使用@target(注解类型全限定名)匹配当前目标对象类型的执行方法, 必须是在目标对象上声明注解,在接口上声明不起作用。@within(M)的匹配规则经验证,目前发现和 @target(M)的匹配规则是一样的。

      PS: @args @target @within没有亲测过,通过多篇他人文章所得。

接下来介绍一下@Before("annotationPointCut()")

      @Before是在所拦截方法执行之前执行一段逻辑。

      @After 是在所拦截方法执行之后执行一段逻辑。

      @Around是可以同时在所拦截方法的前后执行一段逻辑。

      括号中是前面定义了切入点签名的方法的方法名。

      @Around("annotationPointCut()")

      public void around(ProceedingJoinPoint pjp) throws Throwable{

          this.printLog("已经记录下操作日志@Around 方法执行前");

          pjp.proceed(); // 不可少

          this.printLog("已经记录下操作日志@Around 方法执行后");

      }

最后介绍一下内部处理的一些问题,通过反射得到方法的method对象,只能得到一些参数类型,返回类型,方法名,必须从joinPoint中获取实际传入的参数数组:

Object[] params = joinPoint.getArgs();

而获取参数名列表则需要 通过String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();

而通过method可以获取当前的方法名,用于记录该日志是何操作触发的,以供查验。

第三个部分Application,代码如下:


@SpringBootApplication(scanBasePackages = "org.fco")

保证之前写的注解和面向切面的类被纳入管理。

开发中主要遇到了以下问题:

1.在公共工程中切面代码完成后,没有被需要调用的工程的spring容器纳入管理,最后在需要调用公共工程的Application类中加上了SpringbootApplication的注解

2.在记录操作人时,发现注解上不能使用变量,所以只能对需要调用的工程进行简单的处理,在它们的代码中自动帮公共组件填充一个操作人ID,然后在切面所需要执行的代码中获取这个ID并记录到库中。

3.获取触发这个切面的函数参数值和参数名,通过AOP获取的JoinPoint可以顺利的解决这个问题。

相关文章

网友评论

      本文标题:面向切面编程AOP

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