面向切面编程AOP

最近开发中有一个需求,要求对指定的操作进行日志管理,以备以后查验。为了在保全大部分代码结构的前提下,选择了面向切面编程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可以顺利的解决这个问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352