导言
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 分为 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:
+
放在类名后,表示当前类及其子类,放在接口后,表示当前接口及其实现类。
<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.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 有点晕
<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♪(・ω・)ノ