导言
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分为call
、execution
、target
、this
、within
等关键字。与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:
+
放在类名后,表示当前类及其子类,放在接口后,表示当前接口及其实现类。
表达式 | 含义 |
---|---|
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 |
参数 | 含义 |
---|---|
() | 表示方法没有任何参数 |
(..) | 表示匹配接受任意个参数的方法 |
(..,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包安装,安装界面如下:
安装目录用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.class
和com/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的地方,会发现invokestatic
和com/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.11
和org.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(・ω・)ノ