浅谈AOP以及AspectJ和Spring AOP

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。AOP实现的关键就在于AOP框架自动创建的AOP代理,AOP代理则可分为静态代理和动态代理两大类,其中静态代理是指使用AOP框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于`JDK动态代理`、`CGLIB`等在内存中“临时”生成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:

表1 JoinPoint的类型
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: + 放在类名后,表示当前类及其子类,放在接口后,表示当前接口及其实现类。

表2 方法表达式
表达式 含义
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

表3 参数表达式
参数 含义
() 表示方法没有任何参数
(..) 表示匹配接受任意个参数的方法
(..,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/aspectj/downloads.php),在下载页面选择合适的版本下载,目前最新稳定版是1.9.1。下载完之后双加jar包安装,安装界面如下:
AspectJ安装界面

安装目录用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 有点晕



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

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

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

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

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

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

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
13天前
|
XML Java 数据安全/隐私保护
Spring Aop该如何使用
本文介绍了AOP(面向切面编程)的基本概念和术语,并通过具体业务场景演示了如何在Spring框架中使用Spring AOP。文章详细解释了切面、连接点、通知、切点等关键术语,并提供了完整的示例代码,帮助读者轻松理解和应用Spring AOP。
Spring Aop该如何使用
|
1月前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
38 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
19天前
|
Java Spring
[Spring]aop的配置与使用
本文介绍了AOP(面向切面编程)的基本概念和核心思想。AOP是Spring框架的核心功能之一,通过动态代理在不修改原代码的情况下注入新功能。文章详细解释了连接点、切入点、通知、切面等关键概念,并列举了前置通知、后置通知、最终通知、异常通知和环绕通知五种通知类型。
30 1
|
15天前
|
安全 Java 测试技术
Java开发必读,谈谈对Spring IOC与AOP的理解
Spring的IOC和AOP机制通过依赖注入和横切关注点的分离,大大提高了代码的模块化和可维护性。IOC使得对象的创建和管理变得灵活可控,降低了对象之间的耦合度;AOP则通过动态代理机制实现了横切关注点的集中管理,减少了重复代码。理解和掌握这两个核心概念,是高效使用Spring框架的关键。希望本文对你深入理解Spring的IOC和AOP有所帮助。
28 0
|
2月前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
1月前
|
Java 编译器 Spring
Spring AOP 和 AspectJ 的区别
Spring AOP和AspectJ AOP都是面向切面编程(AOP)的实现,但它们在实现方式、灵活性、依赖性、性能和使用场景等方面存在显著区别。‌
77 2
|
1月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
128 9
|
1月前
|
XML Java 数据格式
Spring的IOC和AOP
Spring的IOC和AOP
48 0
|
2月前
|
Java 数据库连接 数据库
Spring基础3——AOP,事务管理
AOP简介、入门案例、工作流程、切入点表达式、环绕通知、通知获取参数或返回值或异常、事务管理
Spring基础3——AOP,事务管理
|
6月前
|
安全 Java Spring
Spring之Aop的底层原理
Spring之Aop的底层原理