乐趣区

浅谈AOP以及AspectJ和Spring-AOP

导言

AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。AOP 实现的关键就在于 AOP 框架自动创建的 AOP 代理,AOP 代理则可分为静态代理和动态代理两大类,其中静态代理是指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于 JDK 动态代理CGLIB 等在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。

面向切面的编程(AOP)是一种编程范式,旨在通过允许横切关注点的分离,提高模块化。AOP 提供切面来将跨越对象关注点模块化。虽然现在可以获得许多 AOP 框架,但在这里我们要区分的只有两个流行的框架:Spring AOP 和 AspectJ。


关键概念

Aspect

Aspect 被翻译方面或者切面,相当于 OOP 中的类,就是封装用于横插入系统的功能。例如日志、事务、安全验证等。

JoinPoint

JoinPoint(连接点)是 AOP 中的一个重要的关键概念。JoinPoint 可以看做是程序运行时的一个执行点。打个比方,比如执行 System.out.println(“Hello”)这个函数,println()就是一个 joinpoint;再如给一个变量赋值也是一个 joinpoint;还有最常用的 for 循环,也是一个 joinpoint。

理论上说,一个程序中很多地方都可以被看做是 JoinPoint,但是 AspectJ 中,只有下面所示的几种执行点被认为是 JoinPoint:

<center> 表 1 JoinPoint 的类型 </center>

JoinPoint 说明 示例
method call 函数调用 比如调用 Logger.info(),这是一处 JoinPoint
method execution 函数执行 比如 Logger.info()的执行内部,是一处 JoinPoint。注意它和 method call 的区别。method call 是调用某个函数的地方。而 execution 是某个函数执行的内部。
constructor call 构造函数调用 和 method call 类似
constructor execution 构造函数执行 和 method execution 类似
field get 获取某个变量 比如读取 User.name 成员
field set 设置某个变量 比如设置 User.name 成员
pre-initialization Object 在构造函数中做得一些工作。
initialization Object 在构造函数中做得工作
static initialization 类初始化 比如类的 static{}
handler 异常处理 比如 try catch(xxx)中,对应 catch 内的执行
advice execution 这个是 AspectJ 的内容

这里列出了 AspectJ 所认可的 JoinPoint 的类型。实际上,连接点也就是你想把新的代码插在程序的哪个地方,是插在构造方法中,还是插在某个方法调用前,或者是插在某个方法中,这个地方就是 JoinPoint,当然,不是所有地方都能给你插的,只有能插的地方,才叫 JoinPoint。

PointCut

PointCut 通俗地翻译为切入点,一个程序会有多个 Join Point,即使同一个函数,也还分为 call 和 execution 类型的 Join Point,但并不是所有的 Join Point 都是我们关心的,Pointcut 就是提供一种使得开发者能够选择自己需要的 JoinPoint 的方法。PointCut 分为 callexecutiontargetthiswithin 等关键字。与 joinPoint 相比,pointcut 就是一个具体的切点。

Advice

Advice 翻译为通知或者增强(Advisor),就是我们插入的代码以何种方式插入,相当于 OOP 中的方法,有 Before、After 以及 Around。

  • Before

前置通知用于将切面代码插入方法之前,也就是说,在方法执行之前,会首先执行前置通知里的代码. 包含前置通知代码的类就是切面。

  • After

后置通知的代码在调用被拦截的方法后调用。

  • Around

环绕通知能力最强,可以在方法调用前执行通知代码,可以决定是否还调用目标方法。也就是说它可以控制被拦截的方法的执行,还可以控制被拦截方法的返回值。

Target

Target 指的是需要切入的目标类或者目标接口。

Proxy

Proxy 是代理,AOP 工作时是通过代理对象来访问目标对象。其实 AOP 的实现是通过动态代理,离不开代理模式,所以必须要有一个代理对象。

Weaving

Weaving 即织入,在目标对象中插入切面代码的过程就叫做织入。


AspectJ

AspectJ 的介绍

AspectJ 是一个面向切面的框架,他定义了 AOP 的一些语法,有一个专门的字节码生成器来生成遵守 java 规范的 class 文件。

AspectJ 的通知类型不仅包括我们之前了解过的三种通知:前置通知、后置通知、环绕通知,在 Aspect 中还有异常通知以及一种最终通知即无论程序是否正常执行, 最终通知的代码会得到执行。

AspectJ 提供了一套自己的表达式语言即切点表达式,切入点表达式可以标识切面织入到哪些类的哪些方法当中。只要把切面的实现配置好,再把这个切入点表达式写好就可以了,不需要一些额外的 xml 配置。

切点表达式语法:

execution(
    modifiers-pattern? // 访问权限匹配   如 public、protected
    ret-type-pattern // 返回值类型匹配
    declaring-type-pattern? // 全限定性类名
    name-pattern(param-pattern) // 方法名(参数名)
    throws-pattern? // 抛出异常类型
)

注意:
1. 中间以空格隔开, 有问号的属性表示可以省略。
2. 表达式中特殊符号说明:

  • a: * 代表 0 到多个任意字符,通常用作某个包下面的某些类以及某些方法。
  • b: .. 放在方法参数中,代表任意个参数,放在包名后面表示当前包及其所有子包路径。
  • c: + 放在类名后,表示当前类及其子类,放在接口后,表示当前接口及其实现类。

<center> 表 2 方法表达式 </center>

表达式 含义
java.lang.String 匹配 String 类型
java.*.String 匹配 java 包下的任何“一级子包”下的 String 类型,如匹配 java.lang.String,但不匹配 java.lang.ss.String
java..* 匹配 java 包及任何子包下的任何类型, 如匹配 java.lang.String、java.lang.annotation.Annotation
java.lang.*ing 匹配任何 java.lang 包下的以 ing 结尾的类型
java.lang.Number+ 匹配 java.lang 包下的任何 Number 的自类型,如匹配 java.lang.Integer,也匹配 java.math.BigInteger

<center> 表 3 参数表达式 </center>

参数 含义
() 表示方法没有任何参数
(..) 表示匹配接受任意个参数的方法
(..,java.lang.String) 表示匹配接受 java.lang.String 类型的参数结束,且其前边可以接受有任意个参数的方法
(java.lang.String,..) 表示匹配接受 java.lang.String 类型的参数开始,且其后边可以接受任意个参数的方法
(*,java.lang.String) 表示匹配接受 java.lang.String 类型的参数结束,且其前边接受有一个任意类型参数的方法

举个栗子:execution(public * com.zhoujunwen.service.*.*(..)),该表达式表示 com.zhoujunwen.service 包下的 public 访问权限的任意类的任意方法。

AspectJ 的安装以及常用命令

AspectJ 下载地址(http://www.eclipse.org/aspect…,在下载页面选择合适的版本下载,目前最新稳定版是 1.9.1。下载完之后双加 jar 包安装,安装界面如下:

安装目录用 tree 命令可以看到如下结构(省去 doc 目录):

├── LICENSE-AspectJ.html
├── README-AspectJ.html
├── bin
│   ├── aj
│   ├── aj5
│   ├── ajbrowser
│   ├── ajc
│   └── ajdoc
└── lib
    ├── aspectjrt.jar
    ├── aspectjtools.jar
    ├── aspectjweaver.jar
    └── org.aspectj.matcher.jar

42 directories, 440 files
  • bin:存放 aj、aj5、ajc、ajdoc、ajbrowser 等命令,其中 ajc 命令最常用,它的作用类似于 javac。
  • doc:存放了 AspectJ 的使用说明、参考手册、API 文档等文档。
  • lib:该路径下的 4 个 JAR 文件是 AspectJ 的核心类库。

注意安装完成后,需要配置将 aspectjrt.jar 配置到 CLASSPATH 中,并且将 bin 目录配置到 PATH 中。下面以 MacOs 配置为例:

JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home
CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:/Users/yourname/Documents/software/aspectj1.9.1/lib/aspectjrt.jar

M2_HOME=/Users/yourname/Documents/software/apache-maven-3.5.0
PATH=$JAVA_HOME/bin:$M2_HOME/bin:/usr/local/bin:/Users/yourname/Documents/software/aspectj1.9.1/bin:$PATH

注意:其中 /Users/yourname/Documents/software/aspectj1.9.1/lib/aspectjrt.jar 替换为自己安装 AspectJ 的路径的 lib,/Users/yourname/Documents/software/aspectj1.9.1/bin替换为安装 AspectJ 的 bin 目录

AspectJ 的 demo

验证 AspectJ 的切面功能,写个单纯的 AspectJ 的 demo,实现方法日志埋点,在方法后增强。

业务代码(AuthorizeService.java):

package com.zhoujunwen.aop;

/**
* 不用太过于较真业务逻辑的处理,大概意思大家懂就好。* @author zhoujunwen
* @version 1.0.0
*/
public class AuthorizeService {
    private static final String USERNAME = "zhoujunwen";
    private static final String PASSWORD = "123456";
    public void login(String username, String password) {if (username == null || username.length() == 0) {System.out.print("用户名不能为空");
            return;
        }
        if (password == null || password.length() == 0) {System.out.print("用户名不能为空");
            return;
        }
        if (!USERNAME.equals(username) || !PASSWORD.equals(password)) {System.out.print("用户名或者密码不对");
            return;
        }
        System.out.print("登录成功");
    }

    public static void main(String[] args) {AuthorizeService as = new AuthorizeService();
        as.login("zhoujunwen", "123456");
    }
}

日志埋点切面逻辑(LogAspect.java):

package com.zhoujunwen.aop;

public aspect LogAspect {pointcut logPointcut():execution(void AuthorizeService.login(..));
    after():logPointcut(){System.out.println("**** 处理日志 ****"); 
    }
} 

将上述两个文件文件放置在同一个目录,在当前目录下执行 acj 编译和织入命令:

ajc -d . AuthorizeService.java LogAspect.java

如果配置一切 OK 的话,不会出现异常或者错误,并在当前目录生成 com/zhoujunwen/aop/AuthorizeService.classcom/zhoujunwen/aop/LogAspect.class两个字节码文件, 执行 tree(自己编写的类似 Linux 的 tree 命令)命令查看目录结构:

zhoujunwendeMacBook-Air:aop zhoujunwen$ tree
.
├── AuthorizeService.java
├── LogAspect.java
└── com
    └── zhoujunwen
        └── aop
            ├── AuthorizeService.class
            └── LogAspect.class

3 directories, 4 files

最后执行 java 执行命令:

java com/zhoujunwen/aop/AuthorizeService

输出日志内容:
登录成功 处理日志

ajc 可以理解为 javac 命令,都用于编译 Java 程序,区别是 ajc 命令可识别 AspectJ 的语法;我们可以将 ajc 当成一个增强版的 javac 命令。执行 ajc 命令后的 AuthorizeService.class 文件不是由原来的 AuthorizeService.java 文件编译得到的,该 AuthorizeService.class 里新增了打印日志的内容——这表明AspectJ 在编译时“自动”编译得到了一个新类,这个新类增强了原有的 AuthorizeService.java 类的功能,因此 AspectJ 通常被称为编译时增强的 AOP 框架

为了验证上述的结论,我们用 javap 命令反编译 AuthorizeService.class 文件。javap 是 Java class 文件分解器,可以反编译(即对 javac 编译的文件进行反编译),也可以查看 java 编译器生成的字节码。用于分解 class 文件。

javap -p -c com/zhoujunwen/aop/AuthorizeService.class

输出内容如下, 在 login 方法的 code 为 0、3 以及 91、94 的地方,会发现 invokestaticcom/zhoujunwen/aop/LogAspect的代码,这说明上面的结论是正确的。

Compiled from "AuthorizeService.java"
public class com.zhoujunwen.aop.AuthorizeService {
  private static final java.lang.String USERNAME;

  private static final java.lang.String PASSWORD;

  public com.zhoujunwen.aop.AuthorizeService();
    Code:
       0: aload_0
       1: invokespecial #16                 // Method java/lang/Object."<init>":()V
       4: return

  public void login(java.lang.String, java.lang.String);
    Code:
       0: invokestatic  #70                 // Method com/zhoujunwen/aop/LogAspect.aspectOf:()Lcom/zhoujunwen/aop/LogAspect;
       3: invokevirtual #76                 // Method com/zhoujunwen/aop/LogAspect.ajc$before$com_zhoujunwen_aop_LogAspect$2$9fd5dd97:()V
       6: aload_1
       7: ifnull        17
      10: aload_1
      11: invokevirtual #25                 // Method java/lang/String.length:()I
      14: ifne          28
      17: getstatic     #31                 // Field java/lang/System.out:Ljava/io/PrintStream;
      20: ldc           #37                 // String 用户名不能为空
      22: invokevirtual #39                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      25: goto          99
      28: aload_2
      29: ifnull        39
      32: aload_2
      33: invokevirtual #25                 // Method java/lang/String.length:()I
      36: ifne          50
      39: getstatic     #31                 // Field java/lang/System.out:Ljava/io/PrintStream;
      42: ldc           #37                 // String 用户名不能为空
      44: invokevirtual #39                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      47: goto          99
      50: ldc           #8                  // String zhoujunwen
      52: aload_1
      53: invokevirtual #45                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      56: ifeq          68
      59: ldc           #11                 // String 123456
      61: aload_2
      62: invokevirtual #45                 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      65: ifne          79
      68: getstatic     #31                 // Field java/lang/System.out:Ljava/io/PrintStream;
      71: ldc           #49                 // String 用户名或者密码不对
      73: invokevirtual #39                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      76: goto          99
      79: getstatic     #31                 // Field java/lang/System.out:Ljava/io/PrintStream;
      82: ldc           #51                 // String 登录成功
      84: invokevirtual #39                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
      87: goto          99
      90: astore_3
      91: invokestatic  #70                 // Method com/zhoujunwen/aop/LogAspect.aspectOf:()Lcom/zhoujunwen/aop/LogAspect;
      94: invokevirtual #73                 // Method com/zhoujunwen/aop/LogAspect.ajc$after$com_zhoujunwen_aop_LogAspect$1$9fd5dd97:()V
      97: aload_3
      98: athrow
      99: invokestatic  #70                 // Method com/zhoujunwen/aop/LogAspect.aspectOf:()Lcom/zhoujunwen/aop/LogAspect;
     102: invokevirtual #73                 // Method com/zhoujunwen/aop/LogAspect.ajc$after$com_zhoujunwen_aop_LogAspect$1$9fd5dd97:()V
     105: return
    Exception table:
       from    to  target type
           6    90    90   Class java/lang/Throwable

  public static void main(java.lang.String[]);
    Code:
       0: new           #1                  // class com/zhoujunwen/aop/AuthorizeService
       3: dup
       4: invokespecial #57                 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #8                  // String zhoujunwen
      11: ldc           #11                 // String 123456
      13: invokevirtual #58                 // Method login:(Ljava/lang/String;Ljava/lang/String;)V
      16: return
}

SpringAOP

Spring AOP 介绍

Spring AOP 也是对目标类增强,生成代理类。但是与 AspectJ 的最大区别在于——Spring AOP 的运行时增强,而 AspectJ 是编译时增强。

dolphin 叔叔 文章中写道自己曾经误以为 AspectJ 是 Spring AOP 的一部分,我想大多数人都没有弄清楚 AspectJ 和 Spring AOP 的关系。

Spring AOP 与 Aspect 无关性

当你不用 Spring AOP 提供的注解时,Spring AOP 和 AspectJ 没半毛钱的关系,前者是 JDK 动态代理,用到了 CGLIB(Code Generation Library),CGLIB 是一个代码生成类库,可以在运行时候动态是生成某个类的子类。代理模式为要访问的目标对象提供了一种途径,当访问对象时,它引入了一个间接的层。后者是静态代理,在编译阶段就已经编译到字节码文件中。Spring 中提供了前置通知 org.springframework.aop.MethodBeforeAdvice、后置通知org.springframework.aop.AfterReturningAdvice,环绕通知org.aopalliance.intercept.MethodInvocation(通过反射实现,invoke(org.aopalliance.intercept.MethodInvocation mi) 中的 MethodInvocation 获取目标方法,目标类,目标字段等信息),异常通知 org.springframework.aop.ThrowsAdvice。这些通知能够切入目标对象,Spring AOP 的核心是代理 Proxy,其主要实现类是org.springframework.aop.framework.ProxyFactoryBean,ProxyFactoryBean 中proxyInterfaces 为代理指向的目标接口,Spring AOP 无法截获未在该属性指定的接口中的方法,interceptorNames是拦截列表,target是目标接口实现类,一个代理只能有一个 target。

Spring AOP 的核心类 org.springframework.aop.framework.ProxyFactoryBean 虽然能实现 AOP 的行为,但是这种方式具有局限性,需要在代码中显式的调用 ProxyFactoryBean 代理工厂类,举例:UserService 是一个接口,UserServiceImpl 是 UserService 的实现类,ApplicationContext context 为 Spring 上下文,调用方式为UserService userService =(UserService)context.getBean("userProxy");

完整的配置如下:

<bean id="userService" class="com.zhoujunwen.UserServiceImpl"></bean>  

<!-- 定义前置通知,com.zhoujunwen.BeforeLogAdvice 实现了 org.springframework.aop.MethodBeforeAdvice -->  
<bean id="beforeLogAdvice" class="com.zhoujunwen.BeforeLogAdvice"></bean>  
<!-- 定义后置通知,com.zhoujunwen.AfterLogAdvice 实现了 org.springframework.aop.AfterReturningAdvice -->  
<bean id="afterLogAdvice" class="com.zhoujunwen.AfterLogAdvice"></bean>  
<!-- 定义异常通知,com.zhoujunwen.ThrowsLogAdvice 实现了 org.springframework.aop.ThrowsAdvice-->  
<bean id="throwsLogAdvice" class="com.zhoujunwen.ThrowsLogAdvice"></bean>  
<!-- 定义环绕通知,com.zhoujunwen.LogAroundAdvice 实现了 org.aopalliance.intercept.MethodInvocation -->  
<bean id="logAroundAdvice" class="com.zhoujunwen.LogAroundAdvice"></bean>  

<!-- 定义代理类,名 称为 userProxy,将通过 userProxy 访问业务类中的方法 -->  
<bean id="userProxy" class="org.springframework.aop.framework.ProxyFactoryBean">  
    <property name="proxyInterfaces">  
        <value>com.zhoujunwen.UserService</value>  
    </property>  
    <property name="interceptorNames">  
        <list>           
         <value>beforeLogAdvice</value>  
         <!-- 织入后置通知 -->  
         <value>afterLogAdvice</value>  
         <!-- 织入异常通知 -->  
         <value>throwsLogAdvice</value>  
         <!-- 织入环绕通知 -->  
         <value>logAroundAdvice</value>  
        </list>  
    </property>  
    <property name="target" ref="userService"></property>  
</bean>

当然,上述的局限性 spring 官方也给出了解决方案,让 AOP 的通知在服务调用方毫不知情的下就进行织入,可以通过 org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator 自动代理。

<bean id="myServiceAutoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">  
        <property name="interceptorNames">  
                <list>
                         <value>logAroundAdvice</value> 
                </list>  
        </property>  
        <property name="beanNames">  
                <value>*Service</value>  
        </property>  
</bean>  

这个 BeanNameAutoProxyCreator 的 bean 中指明上下文中所有调用以 Service 结尾的服务类都会被拦截,执行 logAroundAdvice 的 invoke 方法。同时它会自动生成 Service 的代理,这样在使用的时候就可以直接取服务类的 bean,而不用再像上面那样还用取代理类的 bean。

对于 BeanNameAutoProxyCreator 创建的代理,可以这样调用:UserService userService = (UserService) context.getBean("userService"); ,context 为 spring 上下文。

Spring AOP 与 AspectJ 有关性

当你用到 Spring AOP 提供的注入 @Before、@After 等注解时,Spring AOP 和 AspectJ 就有了关系。在开发中引入了 org.aspectj:aspectjrt:1.6.11org.aspectj:aspectjweaver:1.6.11两个包,这是因为 Spring AOP 使用了 AspectJ 的 Annotation,使用了 Aspect 来定义切面, 使用 Pointcut 来定义切入点,使用 Advice 来定义增强处理。虽然 Spring AOP 使用了 Aspect 的 Annotation,但是并没有使用它的编译器和织入器。

Spring AOP 其实现原理是 JDK 动态代理,在运行时生成代理类。为了启用 Spring 对 @AspectJ 切面配置的支持,并保证 Spring 容器中的目标 Bean 被一个或多个切面自动增强,必须在 Spring 配置文件中添加如下配置

<aop:aspectj-autoproxy/>

当启动了 @AspectJ 支持后,在 Spring 容器中配置一个带 @Aspect 注释的 Bean,Spring 将会自动识别该 Bean,并将该 Bean 作为切面 Bean 处理。切面 Bean 与普通 Bean 没有任何区别,一样使用 <bean.../> 元素进行配置,一样支持使用依赖注入来配置属性值。

Spring AOP 注解使用 demo

全注解实现

业务逻辑代码(AuthorizeService.java):

package com.zhoujunwen.engine.service;

import org.springframework.stereotype.Service;

/**
 * Created with IntelliJ IDEA.
 * Date: 2018/10/25
 * Time: 12:47 PM
 * Description:
 *
 * @author zhoujunwen
 * @version 1.0
 */
@Service
public class AuthorizeService {
    private static final String USERNAME = "zhoujunwen";
    private static final String PASSWORD = "123456";
    public void login(String username, String password) {if (username == null || username.length() == 0) {System.out.print("用户名不能为空");
            return;
        }
        if (password == null || password.length() == 0) {System.out.print("用户名不能为空");
            return;
        }
        if (!USERNAME.equals(username) || !PASSWORD.equals(password)) {System.out.print("用户名或者密码不对");
            return;
        }
        System.out.print("登录成功");
    }
}

切面逻辑代码(LogAspect.java)

package com.zhoujunwen.engine.service;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * Created with IntelliJ IDEA.
 * Date: 2018/10/25
 * Time: 1:04 PM
 * Description:
 *
 * @author zhoujunwen
 * @version 1.0
 */
@Aspect
@Component
public class LogAspect {@After("execution(* com.zhoujunwen.engine.service.AuthorizeService.login(..))")
    public void logPointcut(){System.out.println("*** 处理日志 ***");
    }
}

这样是实现了对 AuthorizeService.login()方法的后置通知。不需要在 xml 中其他配置,当然前提是开启 <aop:aspectj-autoproxy/> aspectj 的自动代理。
测试调用代码:

AuthorizeService authorizeService = SpringContextHolder.getBean(AuthorizeService.class);
authorizeService.login("zhangsan", "zs2018");

xml 配置实现

业务代码,日志埋点(MeasurementService.java):

package com.zhoujunwen.engine.measurement;

import com.zhoujunwen.common.base.AccountInfo;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

/**
 * metrics 切面接口
 * @create 2018-08-16- 上午 10:13
 */
@Service
public class MeasurementService {private static final Logger LOGGER = LoggerFactory.getLogger(MeasurementService.class);

    public String gainZhimaLog(AccountInfo accountInfo) {if (NumberUtils.isNumber(accountInfo.getZhimaPoint())) {return "正常";} else if (StringUtils.contains(accountInfo.getZhimaPoint(), "*")) {return "未授权";} else {return "未爬到";}
    }

    public String gainJiebeiLog(AccountInfo accountInfo) {if (NumberUtils.isNumber(accountInfo.getJiebeiQuota())) {return "正常";}
        return "未爬到";

    }

    public String gainHuabeiLog(AccountInfo accountInfo) {if (accountInfo.getCreditQuota() != null) {return "正常";} else {return "未爬到";}
    }
}

切面逻辑,统计日志中个字段的总和(KeywordMeasurement.java):

package com.zhoujunwen.engine.measurement;

import com.zhoujunwen.common.base.AccountInfo;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;

/**
 * 关键字段监控统计 <br>
 *
 * @create 2018-08-15- 下午 5:41
 */
public class KeywordMeasurement {

    private String invokeCountFieldName = "";
    /**
     * 调用次数
     */
    public void summary(JoinPoint joinPoint, Object result) {
        try {

            String msg;
            String resultStr = "";
            if (result instanceof String) {resultStr = (String) result;
            }
            if (StringUtils.isBlank(resultStr)) {return;}
            if ("正常".equals(resultStr)) {msg = "_ok";} else if ("未爬到".equals(resultStr)) {msg = "_empty";} else {msg = "_star";}

            String methodName = joinPoint.getSignature().getName();
            Object args[] = joinPoint.getArgs();
            AccountInfo accountInfo = null;
            for (Object arg : args) {if (arg.getClass().getName().contains("AccountInfo")) {accountInfo = (accountInfo) arg;
                }
            }
           
            if (methodName.contains("Zhima")) {invokeCountFieldName = "zhima" + msg;} else if (methodName.contains("Jiebei")) {invokeCountFieldName = "jiebei" + msg;} else if (methodName.contains("Huabei")) {invokeCountFieldName = "huabei" + msg;} else {return;}
            // TODO 写入到 influxDB
        } catch (Exception e) {//skip}
    }
}

完整的配置(后置通知,并需要返回结果):

<bean id="keywordMeasurement" class="com.zhoujunwen.engine.measurement.KeywordMeasurement"/>

<aop:config proxy-target-class="true">
    <aop:aspect id="keywordMeasurementAspect" ref="keywordMeasurement">
        <aop:pointcut id="keywordMeasurementPointcut"
                      expression="execution(* com.zhoujunwen.engine.measurement.SdkMeasurementService.gain*(..))"/>
                <!-- 统计 summary,summary 方法有两个参数 JoinPoint 和 Object-->
        <aop:after-returning method="summary" returning="result" pointcut-ref="keywordMeasurementPointcut"/>
    </aop:aspect>
</aop:config>

其他可用的配置(省略了 rt、count、qps 的 aspect):

<!-- 统计 RT,rt 方法只有一个参数 ProceedingJoinPoint-->
<aop:around method="rt" pointcut-ref="keywordMeasurementPointcut"/> 
<!-- 统计调用次数,count 方法只有一个参数 JoinPoint-->
<aop:after method="count" pointcut-ref="keywordMeasurementPointcut"/>
<!-- 统计 QPS,qps 方法只有一个参数 JoinPoint-->
<aop:after method="qps" pointcut-ref="keywordMeasurementPointcut"/>

注意:关于 Spring AOP 中,切面代理类一定是由 Spirng 容器管理,所以委托类也需要交由 Spring 管理,不可以将委托类实例交由自己创建的容器管理(比如放入自己创建的 Map 中), 如果这么做了,当调用委托类实例的时候,切面是不生效的。
原因:(1)实现实现和目标类相同的接口,spring 会使用 JDK 的 java.lang.reflect.Proxy 类,它允许 Spring 动态生成一个新类来实现必要的接口,织入通知,并且把这些接口的任何调用都转发到目标类。
(2)生成子类调用,spring 使用 CGLIB 库生成目标类的一个子类,在创建这个子类的时候,spring 织入通知,并且把对这个子类的调用委托到目标类。


AspectJ 和 Spring AOP 的区别和选择

两者的联系和区别

AspectJ 和 Spring AOP 都是对目标类增强,生成代理类。

AspectJ 是在编译期间将切面代码编译到目标代码的,属于静态代理;Spring AOP 是在运行期间通过代理生成目标类,属于动态代理。

AspectJ 是静态代理,故而能够切入 final 修饰的类,abstract 修饰的类;Spring AOP 是动态代理,其实现原理是通过 CGLIB 生成一个继承了目标类 (委托类) 的代理类,因此,final 修饰的类不能被代理,同样 static 和 final 修饰的方法也不会代理,因为 static 和 final 方法是不能被覆盖的。在 CGLIB 底层,其实是借助了 ASM 这个非常强大的 Java 字节码生成框架。关于 CGLB 和 ASM 的讨论将会新开一个篇幅探讨。

Spring AOP 支持注解,在使用 @Aspect 注解创建和配置切面时将更加方便。而使用 AspectJ,需要通过.aj 文件来创建切面,并且需要使用 ajc(Aspect 编译器)来编译代码。

选择对比

首先需要考虑,Spring AOP 致力于提供一种能够与 Spring IoC 紧密集成的面向切面框架的实现,以便于解决在开发企业级项目时面临的常见问题。明确你在应用横切关注点 (cross-cutting concern) 时(例如事物管理、日志或性能评估),需要处理的是 Spring beans 还是 POJO。如果正在开发新的应用,则选择 Spring AOP 就没有什么阻力。但是如果你正在维护一个现有的应用(该应用并没有使用 Spring 框架),AspectJ 就将是一个自然的选择了。为了详细说明这一点,假如你正在使用 Spring AOP,当你想将日志功能作为一个通知 (advice) 加入到你的应用中,用于追踪程序流程,那么该通知 (Advice) 就只能应用在 Spring beans 的连接点 (Joinpoint) 之上。

另一个需要考虑的因素是,你是希望在编译期间进行织入 (weaving),还是编译后(post-compile) 或是运行时(run-time)。Spring 只支持运行时织入。如果你有多个团队分别开发多个使用 Spring 编写的模块(导致生成多个 jar 文件,例如每个模块一个 jar 文件),并且其中一个团队想要在整个项目中的所有 Spring bean(例如,包括已经被其他团队打包了的 jar 文件)上应用日志通知(在这里日志只是用于加入横切关注点的举例),那么通过配置该团队自己的 Spring 配置文件就可以轻松做到这一点。之所以可以这样做,就是因为 Spring 使用的是运行时织入。

还有一点,因为 Spring 基于代理模式(使用 CGLIB),它有一个使用限制,即无法在使用 final 修饰的 bean 上应用横切关注点。因为代理需要对 Java 类进行继承,一旦使用了关键字 final,这将是无法做到的。在这种情况下,你也许会考虑使用 AspectJ,其支持编译期织入且不需要生成代理。于此相似,在 static 和 final 方法上应用横切关注点也是无法做到的。因为 Spring 基于代理模式。如果你在这些方法上配置通知,将导致运行时异常,因为 static 和 final 方法是不能被覆盖的。在这种情况下,你也会考虑使用 AspectJ,因为其支持编译期织入且不需要生成代理。

如果你希望使用一种易于实现的方式,就选择 Spring AOP 吧,因为 Spring AOP 支持注解,在使用 @Aspect 注解创建和配置切面时将更加方便。而使用 AspectJ,你就需要通过.aj 文件来创建切面,并且需要使用 ajc(Aspect 编译器)来编译代码。所以如果你确定之前提到的限制不会成为你的项目的障碍时,使用 Spring AOP。AspectJ 的一个间接局限是,因为 AspectJ 通知可以应用于 POJO 之上,它有可能将通知应用于一个已配置的通知之上。对于一个你没有注意到这切面问题的大范围应用的通知,这有可能导致一个无限循环。在下面这种情况下,当 proceed 即将被调用时,日志通知会被再次应用,这样就导致了嵌套循环。

public aspectLogging {Object around() : execution(public * * (..))
  Sysytem.out.println(thisJoinPoint.getSignature());
  return proceed();}

参考文章

诚挚感谢以下文章及作者,也是让我在参考实践以及理论总结的过程中学习到了很多东西。不做无头无脑的抄袭者,要做阅读他人的文章,汲取精粹,亲自实践得出结论。尊重原创,尊重作者!

AspectJ(一) 一些该了解的概念
AspectJ 框架,比用 spring 实现 AOP 好用很多哟!
比较分析 Spring AOP 和 AspectJ 之间的差别
AspectJ 基本用法
应用 Spring AOP(一)
AspectJ 官方 doc 文档
Spring AOP,AspectJ, CGLIB 有点晕


<script src=”https://my.openwrite.cn/js/re…; type=”text/javascript”></script>
<script>

const btw = new BTWPlugin();
btw.init({
    id: 'main',
    blogId: '16456-1570500048991-724',
    name: '下雨就像弹钢琴',
    qrcode: 'https://www.zhoujunwen.com/wp-content/uploads/2019/09/qrcode_for_gh_f7c7e6ace303_258-1.jpg',
    keyword: '阅读全文',
});

</script>


该文首发《虚怀若谷》个人博客,转载前请务必署名,转载请标明出处。

古之善为道者,微妙玄通,深不可识。夫唯不可识,故强为之容:

豫兮若冬涉川,犹兮若畏四邻,俨兮其若客,涣兮若冰之释,敦兮其若朴,旷兮其若谷,混兮其若浊。

孰能浊以静之徐清?孰能安以动之徐生?

保此道不欲盈。夫唯不盈,故能敝而新成。

请关注我的微信公众号:下雨就像弹钢琴,Thanks♪(・ω・)ノ

退出移动版