AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。AOP实现的关键就在于AOP框架自动创建的AOP代理,AOP代理则可分为静态代理和动态代理两大类,其中静态代理是指使用AOP框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于 JDK动态代理
、 CGLIB
等在内存中“临时”生成AOP动态代理类,因此也被称为运行时增强。
面向切面的编程(AOP) 是一种编程范式,旨在通过允许横切关注点的分离,提高模块化。AOP提供切面来将跨越对象关注点模块化。虽然现在可以获得许多AOP框架,但在这里我们要区分的只有两个流行的框架:Spring AOP和AspectJ。
Aspect被翻译方面或者切面,相当于OOP中的类,就是封装用于横插入系统的功能。例如日志、事务、安全验证等。
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通俗地翻译为切入点,一个程序会有多个Join Point,即使同一个函数,也还分为call和execution类型的Join Point,但并不是所有的Join Point都是我们关心的,Pointcut就是提供一种使得开发者能够选择自己需要的JoinPoint的方法。PointCut分为 call
、 execution
、 target
、 this
、 within
等关键字。与joinPoint相比,pointcut就是一个具体的切点。
Advice翻译为通知或者增强(Advisor),就是我们插入的代码以何种方式插入,相当于OOP中的方法,有Before、After以及Around。
前置通知用于将切面代码插入方法之前,也就是说,在方法执行之前,会首先执行前置通知里的代码.包含前置通知代码的类就是切面。
后置通知的代码在调用被拦截的方法后调用。
环绕通知能力最强,可以在方法调用前执行通知代码,可以决定是否还调用目标方法。也就是说它可以控制被拦截的方法的执行,还可以控制被拦截方法的返回值。
Target指的是需要切入的目标类或者目标接口。
Proxy是代理,AOP工作时是通过代理对象来访问目标对象。其实AOP的实现是通过动态代理,离不开代理模式,所以必须要有一个代理对象。
Weaving即织入,在目标对象中插入切面代码的过程就叫做织入。
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. 表达式中特殊符号说明:
* .. +
<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下载地址( 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
注意安装完成后,需要配置将 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的切面功能,写个单纯的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 }
Spring AOP也是对目标类增强,生成代理类。但是与AspectJ的最大区别在于——Spring AOP的运行时增强,而AspectJ是编译时增强。
dolphin叔叔文章中写道自己曾经误以为AspectJ是Spring AOP的一部分,我想大多数人都没有弄清楚AspectJ和Spring AOP的关系。
当你不用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提供的注入@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.../>
元素进行配置,一样支持使用依赖注入来配置属性值。
业务逻辑代码(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");
业务代码,日志埋点(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是静态代理,故而能够切入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♪(・ω・)ノ