浅谈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:

表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-initializationObject在构造函数中做得一些工作。
initializationObject在构造函数中做得工作
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包安装,安装界面如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2IdKl9xs-1570714418032)(/wp-content/uploads/2018/10/aspectj-install.png)]

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

输出日志内容:
登录成功处理日志
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eM0Olmpz-1570714418034)(/wp-content/uploads/2018/10/aspectj-compiler-run-demo.png)]

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♪(・ω・)ノ
微信二维码

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Joyven2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值