什么是AOP?
AOP又名Aspect Oriented Programming 意为 ‘面向切面编程’通过预编译和运行期间动态代理来实现程序功能的统一维护的一种技术。AOP思想是OOP(面向对象)的延续 在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面),AOP是软件行业的热点,也是Spring框架中的一个重要内容,是函数式编程的一种延伸范式,
总结:这种在运行时生成代理对象来织入的,还可以在编译期、类加载期织入,动态地将代码在不改变原有的逻辑情况下切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用
但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?
即假如一个流程分三个步骤,分别是X,A,Y,另一个流程的三个步骤是X,B,Y。 写在程序里,两个方法体分别是XAY和XBY,
显然,这出现了重复,违反了DRY原则。 你可以把X和Y分别抽成一个方法,但至少还是要写一条语句来调用方法,xAy,xBy,
重复依然存在。 如果控制反转来处理这问题,将采用模板方法的模式,在抽象父类方法体中声明x?y,其中?部分为抽象方法,由具体子类实现。 但这就出现了继承,而且调用者只能调用父类声明的方法,耦合性太强,不灵活。 所以,我们常看到,只有那些本来就是调用者调用父类声明的方法的情况,比如表现层,或者本来就不用太灵活,比如只提供增删改查的持久层,才总出现抽象父类的身影。
具体Controller is-a 抽象Controller,具体Dao is-a 抽象Dao,这大家都能接受。 但除了在抽象Controller、抽象Dao中固定的步骤之外,我们就不需要点别的吗? 比如在某些Controller方法运行之前做点什么,在某些Dao方法运行之前之后做点什么? 而且最好能基于配置,基于约定,而不是都死乎乎硬编码到代码里。
于是乎 面向切面横空出世
一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。
AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。
AOP体系:
术语
Aspect(切面)
aspect 由 pointcount 和 advice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中.
AOP的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:如何通过 pointcut 和 advice 定位到特定的 joinpoint 上
如何在 advice 中编写切面代码.
可以简单地认为, 使用 @Aspect 注解的类就是切面.
总结:切面(aspect):通知(即增强)和切点的结合。
连接点(join point)
a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.
程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.
在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.
总结:连接点(join point):在应用执行过程中能够插入切面的一个点。(注:就是抽象的「切点」声明所指代的那些具体的点。)
JointPoint是程序运行过程中可识别的点,这个点可以用来作为AOP切入点。JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。
切点(point cut)
匹配 join point 的谓词(a predicate that matches join points).
Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行.
在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而
总结:切点(pointcut):一组连接点的总称,用于指定某个增强应该在何时被调用
通俗一点讲:
pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.
pointcut 是一种程序结构和规则,它用于选取join point并收集这些point的上下文信息。
pointcut通常包含了一系列的Joint Point,我们可以通过pointcut来同时操作jointpoint。单从概念上,可以把Pointcut当做jointpoint的集合。
关于join point 和 point cut 的区别
在 Spring AOP 中, 所有的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 因此 join point 和 point cut 本质上就是两个不同纬度上的东西.
总结:advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice
introduction
为一个类型添加额外的方法或字段. Spring AOP 允许我们为 目标对象 引入新的接口(和对应的实现). 例如我们可以使用 introduction 来为一个 bean 实现 IsModified 接口, 并以此来简化 caching 的实现.
目标对象(Target)
织入 advice 的目标对象. 目标对象也被称为 advised object.
因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)
注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.
AOP proxy
一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和增强逻辑的代理类.
在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象.
织入(Weaving)
将 aspect 和其他对象连接起来, 并创建 adviced object 的过程.
根据不同的实现技术, AOP织入有三种方式:编译器织入, 这要求有特殊的Java编译器.
类装载期织入, 这需要有特殊的类装载器.
动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.
Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.
advice 的类型before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)
after return advice, 在一个 join point 正常返回后执行的 advice
after throwing advice, 当一个 join point 抛出异常后执行的 advice
after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.
关于 AOP Proxy
Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 通过它, 我们可以为任意的接口实现代理.
如果需要为一个类实现代理, 那么可以使用 CGLIB 代理. 当一个业务逻辑对象没有实现接口时, 那么Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了. 即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类.
彻底理解 aspect, join point, point cut, advice
看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的.
下面我以一个简单的例子来比喻一下 AOP 中 aspect, jointpoint, pointcut 与 advice 之间的关系.
让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.
来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系.
首先我们知道, 在 Spring AOP 中 join point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, join point 就相当于 爪哇的小县城里的百姓, point cut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 advice 则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问.
为什么可以这样类比呢?
join point –>
爪哇的小县城里的百姓: 因为根据定义, join point 是所有可能被织入 advice 的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是 join point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.
point cut –>
男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 advice, 但是我们并不希望在所有方法上都织入 advice, 而 pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问.
advice –>
抓过来审问, advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 join point 上的. 同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓.
aspect: aspect 是 point cut 与 advice 的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个 aspect.
或则我们也可以从语法的角度来简单类比一下. 我们在学英语时, 经常会接触什么 定语, 被动句 之类的概念, 那么可以做一个不严谨的类比, 即 joinpoint 可以认为是一个 宾语, 而 pointcut 则可以类比为修饰 joinpoint 的定语, 那么整个 aspect 就可以描述为: 满足 pointcut 规则的 joinpoint 会被添加相应的 advice 操作.
增强(advice,另译为通知,但《3.x》作者不赞成):在特定连接点执行的动作。
我们为什么要使用AOP?
我们可以利用AOP的思想来对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
在Spring AOP中业务逻辑仅仅只关注业务本身,将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
AOP相当于一个拦截器,去拦截一些处理,例如:当一个方法执行的时候,Spring 能够拦截正在执行的方法,在方法执行的前或者后增加额外的功能和处理,就是我们希望通过使用AOP来对我们的代码进行耦合度的降低 把原本杂乱交错的关系给分离开来 进而改变这些行为的时候不会因为杂乱的关系互相影响。
AOP应该怎样使用?
老规矩导依赖
org.springframework.boot
spring-boot-starter-aop
test
定义 aspect(切面)
@Aspect:作用是把当前类标识为一个切面供容器读取
如果一个类被打上了@Aspect就代表着他是一个切面类
当使用注解 @Aspect 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如:
@Aspect//定义该类为切面
@Component
public class AspectDemo
{
}
注意, 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Bean, 因此我们还需要使用类似 @Component 之类的注解.
注意, 如果一个 类被@Aspect 标注, 则这个类就不能是其他 aspect 的 advised object(目标对象) 了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying(自动代理) 机制之外.
声明 pointcut(切入点)
一个 pointcut 的声明由两部分组成:
一个方法签名, 包括方法名和相关参数
一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice).
package com.example.javalogframe.text;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect//定义该类为切面
@Component
public class AspectDemo
{
@Pointcut("execution(* com.example.javalogframe.text.Jui_LogDemo.*(..))")//切点表达式
public void aspectPointcut()
{
}
}
这个方法必须无返回值.
这个方法本身就是 pointcut signature(签名), pointcut 表达式使用@Pointcut 注解指定.
上面我们简单地定义了一个 pointcut, 这个 pointcut 所描述的是: 匹配所有在包 com.example.javalogframe.text.Jui_LogDemo 下的所有方法的执行.
切点标志符(designator)
AspectJ5 的切点表达式由标志符(designator)和操作参数组成. 如 “execution(* greetTo(..))” 的切点表达式, execution 就是 标志符, 而圆括号里的 * greetTo(..) 就是操作参数
execution(执行)
匹配 join point 的执行, 例如 “execution(* hello(..))” 表示匹配所有目标类中的 hello() 方法. 这个是最基本的 pointcut 标志符.
within(内)
匹配特定包下的所有 join point, 例如 within(com.xys.*) 表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法. 而 within(com.xys.demo2.*Service) 表示在 com.xys.demo2包中所有以 Service 结尾的类的所有的连接点.
@Pointcut("within(com.xys.demo2.*)")
public void pointcut2() {
}
this 与 target
this 的作用是匹配一个 bean, 这个 bean(Spring AOP proxy) 是一个给定类型的实例(instance of). 而 target 匹配的是一个目标对象(target object, 即需要织入 advice 的原始的类), 此对象是一个给定类型的实例(instance of).
bean
匹配 bean 名字为指定值的 bean 下的所有方法, 例如:
bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法
bean(myService) // 匹配名字为 myService 的 bean 下的所有方法
args
匹配参数满足要求的的方法.
例如:
切面类
package com.example.javalogframe.text;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
@Aspect//定义该类为切面
@Component
public class AspectDemo
{
@Pointcut("within(com.example.javalogframe.text.*)")//切点表达式
public void aspectPointcut()
{
}
@Before(value = "aspectPointcut() && args(name)")
public void outName(String name){
System.out.println("OUTNAME:"+name);
}
}
方法类
package com.example.javalogframe.text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class AspectText
{
private static final Logger l = LoggerFactory.getLogger(AspectText.class);
public void a()
{
l.info("我出来咯表哥");
}
public String b(String name){
l.info("name:{}",name);
return "可以咯";
}
}
启动类
package com.example.javalogframe.text;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.logging.Logger;
@Component
public class Jui_LogDemo
{
private static final Logger LOGGER = Logger.getLogger(Jui_LogDemo.class.getName());
@Autowired
AspectText aspectText;
public @PostConstruct void Log()
{
aspectText.b("帅");
}
}
不能在mian方法中调用也不能在自身的类里面调用b类 那样不算从Spring 容器中拿到的对象 所以AOP就会失效
输出效果:
. ____ _ __ _ _
/\ / ___'_ __ _ _(_)_ __ __ _
( ( )___ | '_ | '_| | '_ / _` |
\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |___, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.0)
11:29:52 [main] INFO com.example.javalogframe.JavaLogframeApplication - Starting JavaLogframeApplication using Java 1.8.0_301 on DESKTOP-KSHCS0C with PID 27016 (E:LHJJava练习java-logframetargetclasses started by Administrator in E:LHJJava练习java-logframe)
11:29:52 [main] INFO com.example.javalogframe.JavaLogframeApplication - No active profile set, falling back to 1 default profile: "default"
OUTNAME:帅
11:29:53 [main] INFO com.example.javalogframe.text.AspectText - name:帅
11:29:53 [main] INFO com.example.javalogframe.JavaLogframeApplication - Started JavaLogframeApplication in 0.663 seconds (JVM running for 1.319)
_____ _ _ log服务启动成功 _ _ _____ _______ _ ____ _ _
/ ____| | | | / | | |/ ____|___ / | | |/ __ | | | |
| (___ | |__| | / | | | | __ / /| |__| | | | | | | |
___ | __ | / / | . ` | | |_ | / / | __ | | | | | | |
____) | | | |/ ____ | | | |__| |/ /__| | | | |__| | |__| |
|_____/|_| |_/_/ __| _|_____/_____|_| |_|____/ ____/
我的项目结构图
当 aspectText.b 执行时, 则 advice outName()就会执行, test 方法的参数 name 就会传递到 outName中.
所以我在切面类里面用within是去拿到text包下的所有方法,然后我在通过@Before前置增强在用args参数匹配找到匹配参数的方法然后输出拿到的参数
常用例子:
// 匹配只有一个参数 name 的方法
@Before(value = "aspectMethod() && args(name)")
public void doSomething(String name) {
}
// 匹配第一个参数为 name 的方法
@Before(value = "aspectMethod() && args(name, ..)")
public void doSomething(String name) {
}
// 匹配第二个参数为 name 的方法
Before(value = "aspectMethod() && args(*, name, ..)")
public void doSomething(String name) {
}
@annotation
匹配由指定注解所标注的方法, 例如:
@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut()
{
}
则匹配由注解 AuthChecker 所标注的方法.
常见的切点表达式
// 匹配指定包中的所有的方法
execution(* com.xys.service.*(..))
// 匹配当前包中的指定类的所有方法
execution(* UserService.*(..))
// 匹配指定包中的所有 public 方法
execution(public * com.xys.service.*(..))
// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法
execution(public int com.xys.service.*(..))
// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法
execution(public int com.xys.service.*(String name, ..))
匹配类型签名
// 匹配指定包中的所有的方法, 但不包括子包
within(com.xys.service.*)
// 匹配指定包中的所有的方法, 包括子包
within(com.xys.service..*)
// 匹配当前包中的指定类中的方法
within(UserService)
// 匹配一个接口的所有实现类中的实现的方法
within(UserDao+)
匹配 Bean 名字
// 匹配以指定名字结尾的 Bean 中的所有方法
bean(*Service)
切点表达式组合
// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)
// 匹配名字以 Service 开头, 并且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)
声明 advice
advice 是和一个 pointcut 表达式关联在一起的, 并且会在匹配的 join point 的方法执行的前/后/周围 运行. pointcut 表达式可以是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式.
下面我们以几个简单的 advice 为例子, 来看一下一个 advice 是如何声明的.
Before advice
/**
* @author xiongyongshun
* @version 1.0
* @created 16/9/9 13:13
*/
@Component
@Aspect
public class BeforeAspectTest {
// 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
@Pointcut("execution(* com.xys.service.UserService.*(..))")
public void dataAccessOperation() {
}
}
@Component
@Aspect
public class AdviseDefine {
// 定义 advise
@Before("com.xys.aspect.PointcutDefine.dataAccessOperation()")
public void doBeforeAccessCheck(JoinPoint joinPoint) {
System.out.println("*****Before advise, method: " + joinPoint.getSignature().toShortString() + " *****");
}
}
这里, @Before 引用了一个 pointcut, 即 “com.xys.aspect.PointcutDefine.dataAccessOperation()” 是一个 pointcut 的名字.
如果我们在 advice 在内置 pointcut, 则可以:
@Component
@Aspect
public class AdviseDefine {
// 将 pointcut 和 advice 同时定义
@Before("within(com.xys.service..*)")
publicvoiddoAccessCheck(JoinPoint joinPoint)
{
System.out.println("*****doAccessCheck, Before advise, method: " + joinPoint.getSignature().toShortString() + " *****");
}
}
around advice
around advice 比较特别, 它可以在一个方法的之前之前和之后添加不同的操作, 并且甚至可以决定何时, 如何, 是否调用匹配到的方法.
在看环绕增强时我们需要先了解两个对象 joinpoint(连接点) 和 proceedingjoinpoint(进行连接点)
因为我们前面了解到 joinpoint 是连接点的意思 所以 JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。
而proceedingjoinpoint 继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。
JointPoint和ProceedingJoinPoint区别?
JointPoint
通过JpointPoint对象可以获取到下面信息
# 返回目标对象,即被代理的对象
Object getTarget();
# 返回切入点的参数
Object[] getArgs();
# 返回切入点的Signature
Signature getSignature();
# 返回切入的类型,比如method-call,field-get等等,感觉不重要
String getKind();
ProceedingJoinPoint
Proceedingjoinpoint 继承了 JoinPoint。是在JoinPoint的基础上暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。
环绕通知=前置+目标方法执行+后置通知,proceed方法就是用于启动目标方法执行的
暴露出这个方法,就能支持 aop:around 这种切面(而其他的几种切面只需要用到JoinPoint,,这也是环绕通知和前置、后置通知方法的一个最大区别。这跟切面类型有关), 能决定是否走代理链还是走自己拦截的其他逻辑。建议看一下 JdkDynamicAopProxy的invoke方法,了解一下代理链的执行原理。
JointPoint使用详解
这里详细介绍JointPoint的方法,这部分很重要是coding核心参考部分。开始之前我们思考一下,我们到底需要获取切入点的那些信息。我的思考如下
- 切入点的方法名字及其参数
- 切入点方法标注的注解对象(通过该对象可以获取注解信息)
- 切入点目标对象(可以通过反射获取对象的类名,属性和方法名)
注:有一点非常重要,Spring的AOP只能支持到方法级别的切入。换句话说,切入点只能是某个方法。
针对以上的需求JDK提供了如下API
1 获取切入点所在目标对象
// 获取切入点所在目标对象
Object targetObj =joinPoint.getTarget();
//可以发挥反射的功能获取关于类的任何信息,例如获取类名如下
String className = joinPoint.getTarget().getClass().getName();
因为一个类有很多方法,为了获取具体切入点所在的方法可以通过如下API
2.获取切入点方法的名字
getSignature());是获取到这样的信息 :修饰符+ 包名+组件名(类名) +方法名
这里我只需要方法名
String methodName = joinPoint.getSignature().getName()
3. 获取方法上的注解
方法1:xxxxxx是注解名字
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null)
{
xxxxxx annoObj= method.getAnnotation(xxxxxx.class);
}
return null;
方法2:上面我们已经知道了方法名和类的对象,通过反射可以获取类的内部任何信息。
// 切面所在类
Object target = joinPoint.getTarget();
String methodName = joinPoint.getSignature().getName();
Method method = null;
for (Method m : target.getClass().getMethods()) {
if (m.getName().equals(methodName)) {
method = m;
// xxxxxx annoObj= method.getAnnotation(xxxxxx.class);同上
break;
}
}
4. 获取方法的参数
这里返回的是切入点方法的参数列表
Object[] args = joinPoint.getArgs();
测试
眼见为实,测试一遍可以理解更深刻
注解类
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiLog
{
/**
* 模块
*/
public String title() default "";
/**
* 日志记录service实现
* @return
*/
public String logService() default "operLogServiceImpl";
/**
* 是否保存请求的参数
*/
public boolean isSaveRequestData() default true;
/**
* 是否追踪用户操作
* @return
*/
public boolean isTrack() default true;
}
切面类
package com.kouryoushine.aop.test;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@Aspect
@Component
public class DemoAspect {
//切入点:aopdemo报下所有对象的save方法
@Pointcut("execution(public * com.kouryoushine.aop.test.*.save*(..))")
public void save(){
}
/**
* 需要在update操作前后分别获取更新前后的值
* @param
* @return
*/
@AfterReturning("save()")
public void afterReturn(JoinPoint joinPoint) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
//1.获取切入点所在目标对象
Object targetObj =joinPoint.getTarget();
System.out.println(targetObj.getClass().getName());
// 2.获取切入点方法的名字
String methodName = joinPoint.getSignature().getName();
System.out.println("切入方法名字:"+methodName);
// 3. 获取方法上的注解
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
if (method != null)
{
ApiLog apiLog= method.getAnnotation(ApiLog.class);
System.out.println("切入方法注解的title:"+apiLog.title());
}
//4. 获取方法的参数
Object[] args = joinPoint.getArgs();
for(Object o :args){
System.out.println("切入方法的参数:"+o);
}
}
}
服务类
@Service
public class TestServcie {
@ApiLog(title = "注解的标题",isSaveRequestData = false)
public void save(String parm1,int parm2){
System.out.println("执行目标对象的方法"+parm1+parm2);
}
public void update(){
System.out.println("没有注解的方法,不会被拦截");
}
}
测试方法
@Autowired
TestServcie testServcie;
@Test
void test6() throws Exception{
testServcie.save("参数1字符串",33);
}
测试结果
com.kouryoushine.aop.test.TestServcie
切入方法名字:save
切入方法注解的title:注解的标题
切入方法的参数:参数1字符串
切入方法的参数:33
所以现在我们知道了joinpoint 对象的作用 可以获取切入点的消息
而proceedingjoinpoint 仅支持环绕增强建议
因为proceedingjoinpoint和joinpoint不同的暴露出 proceed 这个方法。proceed很重要,这个是aop代理链执行的方法。
@Component
@Aspect
public class AdviseDefine {
// 定义 advise
@Around("com.xys.aspect.PointcutDefine.dataAccessOperation()")
public Object doAroundAccessCheck(ProceedingJoinPoint pjp) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 开始
Object retVal = pjp.proceed();
stopWatch.stop();
// 结束
System.out.println("invoke method: " + pjp.getSignature().getName() + ", elapsed time: " + stopWatch.getTotalTimeMillis());
return retVal;
}
}
Advice(通知、切面): 某个连接点所采用的处理逻辑,也就是向连接点注入的代码, AOP在特定的切入点上执行的增强处理。意思就是advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 join point 上的
相关注解介绍:
@Around:环绕增强,相当于MethodInterceptor
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
@Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
@AfterThrowing:异常抛出增强,相当于ThrowsAdvice
@After: final增强,不管是抛出异常或者正常退出都会执行
1.@Before(目标方法执行之前)
在advice中内容是int,div调用之前输出的,因而用到了@Before
创建新的java类ComputerAop,并添加@Aspect与@Component注释。
public class ComputerAop{
//目标方法执行之前
@Before("execution(public int xxx.xx.xxxxx.xxxxx.ComputerService.*(..))")
//xxx ComputerService前的路径。它会自动寻找ComputerService中所有public int的方法。
public void before(JoinPoint jp) {
Object[] args=jp.getArgs();
//获取目标方法对应参数
Signature sg=jp.getSignature();
//获取目标方法
String name=sg.getName();
System.out.println(this.getClass().getName()+":the "+name+" method begins.");
System.out.println(this.getClass().getName()+":parameters of the "+name+" method["+args[0]+","+args[1]+"].");
}
}
执行结果如下
2.@After(目标方法执行完,无论是否出现错误异常,都会执行)
@After("execution(public int xxx.xx.xxxxx.xxxxx.ComputerService.*(..))")
public void after(JoinPoint jp){
Signature sg=jp.getSignature();
String name=sg.getName();
System.out.println(this.getClass().getName()+":The "+name+"method ends");
}
执行结果如下
3.@AfterReturning(目标方法返回结果时,出现错误异常,不会执行)
@AfterReturning(value="("execution(public int xxx.xx.xxxxx.xxxxx.ComputerService.*(..))")",returning="result")
public void afterReturning(JoinPoint jp,Object result) {
Signature sg=jp.getSignature();
String name=sg.getName();
System.out.println(this.getClass().getName()+"the result of :"+name+" is "+result);
}
当div方法传参数(1,1)时,return结果无措,正常执行。
当div方法传参数(1,0)时,return结果有措,不执行。
4.@AfterThrowing(目标方法出现错误异常执行)
@AfterThrowing(value="execution(public int xxx.xx.xxxxx.xxxxx.ComputerService.*(..))",throwing="e")
public void afterThrowing(JoinPoint jp,Exception e) {
System.out.println(e.getMessage());
}
结果如下,打印出了错误。
5.@Around(可以实现以上所有功能)
@Around(value = "execution(public int xxx.xx.xxxxx.xxxxx.ComputerService.*(..))")
public object around(ProceedingJoinPoint pjp) {
object [] args = pjp.getArgs();//传入目标方法的参数
signature sg = pjp.getSignature();
String name = sg.getName();
System.out .print1n( this. getClass().getName()+": The "+name+" method begins.");
System.out.println(this.getClass().getName()+": Parameters of the "+name+" method: [" +args[0]+"," +args[1]+"]");
try {
try {
object object = pjip. getTarget();//目标类创建的对象
System.out.print1n("*******" +object. getClass() . getName());
object result =pjp. proceed();//调用目标方法,并且返回目标方法的结果
System.out.println(this.getClass() .getName()+": Result of the "+name+" method: " +result);
return result;
}finally {
System.out.println(this.getClass().getName()+": The "+name+" method ends.");
} catch (Throwable e) {
System.out.println(e.getMessage();|
return -1;
}
我们为什么要使用AOP?
举个例子,你想给你的网站加上鉴权,
对某些url,你认为不需要鉴权就可以访问,
对于某些url,你认为需要有特定权限的用户才能访问
如果你依然使用OOP,面向对象,
那你只能在那些url对应的Controller代码里面,一个一个写上鉴权的代码而如果你使用了AOP呢?
那就像使用Spring Security进行安全管理一样简单(更新:Spring Security的拦截是基于Servlet的Filter的,不是aop,不过两者在使用方式上类似):
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/static","/register").permitAll()
.antMatchers("/user/**").hasRoles("USER", "ADMIN")
这样的做法,对原有代码毫无入侵性,这就是AOP的好处了,把和主业务无关的事情,放到代码外面去做。
所以当你下次发现某一行代码经常在你的Controller里出现,比如方法入口日志打印,那就要考虑使用AOP来精简你的代码了。
AOP是OOP的有益补充
基于Java语言的web开发,本质是用面向对象的组织,面向过程的逻辑,来解决问题。应用实践中灵活具体,不拘泥,不教条。
总结: Spring实现的AOP是代理模式,给调用者使用的实际是已经过加工的对象,你编程时方法体里只写了A,但调用者拿到的对象的方法体却是xAy。x和y总还是需要你来写的,这就是增强。x和y具体在什么时候被调用总还是需要你来规定的,虽然是基于约定的声明这种简单的规定,这就是切点。