1 AOP概述
思考:在一个教务系统中,以下哪些是主要业务逻辑,哪些是次要业务逻辑?
1.1 关于AOP
AOP(面向方面/切面编程)是对OOP(面向对象编程)的补充,提供另一种关于程序结构的思维方式。OOP中模块化的关键单元是类,而AOP中模块化的单元是方面/切面(Aspect)。方面允许将横切多个类型和对象的关注点(如事务管理)模块化,在AOP的术语中,这种关注点通常叫做“横切关注点”。
AOP允许将次要业务逻辑相关的横切关注点模块化为方面,然后将方面切入到需要的目标对象中。
AOP实现将次要业务逻辑从主要业务逻辑中分离,从而降低程序耦合性和提高代码重用,让开发人员可以专注主要业务逻辑开发。
该思想的核心是:
- AOP将软件系统分成两部分:核心关注点(主要业务逻辑)和横切关注点(次要业务逻辑)
- 将次要逻辑(日志记录、安全、事务管理等)从主要/核心业务逻辑中分离出来,做成单独的模块,也就是方面
- 然后在需要切入次要逻辑的地方切入即可,这样核心业务模块专注于主要业务逻辑即可
- 通俗地说,是在运行时,动态地将代码切入到类的指定方法或其他指定位置上的一种编程思想
1.2 AOP术语
1.2.1 常用术语
实现AOP的过程中,我们会用到各种各样的组件和过程,我们使用不同的术语称呼不同的组件和过程,这些术语并非由Spring提供,而是在AOP中已经广泛使用的术语,Spring沿用了这些术语:
- 切面/方面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是一个关于横切关注点的很好的例子。
- 连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
- 通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知。
- 切入点(Pointcut):应用通知的连接点。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行。
- 目标对象(Target Object): 被一个或者多个方面所通知的对象。也被称做被通知(advised)对象,也就是通知应用到的目标对象。 Spring AOP是通过运行时动态代理实现的,这个对象永远是一个被代理(proxied)对象。
- AOP代理(AOP Proxy):代理对象,也就是AOP框架创建的对象,用来实现方面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
- 织入(Weaving):将方面和目标对象进行关联,并创建被通知对象(代理对象)的过程叫做织入。
对于连接点和切入点,一言以蔽之:
- 连接点(Join Point):可以应用通知的点,如方法的执行或跑出异常,spring只支持方法执行作为连接点
- 切入点 (Pointcut):选中要应用通知的连接点构成切入点
1.2.2 通知
通知类型:
- 前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
- 返回后通知(After returning advice):在某连接点正常完成后执行的通知,例如,一个方法没有抛出任何异常,正常返回。
- 抛出后通知(After throwing advice):在方法抛出异常退出时执行的通知。
- 后置/最终通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
- 环绕通知(Around Advice):包围一个连接点执行的通知,如方法调用。这是最强大的一种通知。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。
如果不同方面中的两个或两个以上的通知应用到同一个连接点,除非指定了执行顺序,否则这多个通知的执行顺序是未知的,可以通过让方面类实现Ordered接口或使用@Order注解指定方面中通知的执行顺序。顺序数字越小,表示执行的优先级越高
1.2.3 切入点
1.2.3.1 语法格式
定义切入点要定义两部分:
- 切入点签名:一个方法声明,方法名为切入点名称,方法可以有任意参数,但是返回类型必须是null
- 切入点表达式:一个表达式,指定切入点覆盖的连接点,这里使用的是AspectJ的切入点写法
// 切入点表达式
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
// 切入点方法签名
public void pointcut1(){}
切入点表达式的格式为:
-
execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
-
public:访问修饰符,还可以是private等,可以省略
- int:返回类型
- com.qdu.service.MathService:类/接口名称
- add:方法名
- (int, int):方法参数,直接写参数的类型,多个类型用逗号隔开
-
异常名:方法定义中抛出指定异常,可以省略
1.2.3.2 通配符
我们使用通配符描述切入点,主要的目的就是简化之前的配置,具体都有哪些通配符可以使用?
*:
单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
execution(* com.qdu.service.impl.StudentServiceImpl.*(String))
execution(* com.qdu.service.StudentService.*(String,String))
..:
多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
execution(* com.qdu.service.StudentService.*(..))
execution(* com.qdu..*.*(..))
1.2.3.3 实例
声明切入点的格式:一个切入点表达式+方法签名
方法名是引用这个切入点的名称,也就是切入点的名称
@Aspect
@Component
public class LogAspect {
@Pointcut("@within(com.qdu.aop.LogJoinPoint) || @annotation(com.qdu.aop.LogJoinPoint)")
public void pointcut() {
}
@Before("pointcut()")
public void beforeAdvice(JoinPoint point) {
System.out.println("---------前置通知,方法名称: "+point.getSignature().getName());
}
@AfterReturning("pointcut()")
public void AfterReturningAdvice(JoinPoint p) {
System.out.println("---------返回后通知,方法名称: "+p.getSignature().getName());
}
}
在定义切入点表达式中我们发现在@Pointcut中我们用的最多的是execution,上面的例子也用了@within、@annotation,这是标志符。定义切入点表达式时一般会用到以下几种标志符:
- execution标志符:指定方法执行作为连接点,具体到方法
- within标志符:指定连接点所在的类型,具体到类型(实现类)
- target标志符:指定目标对象对应的类型,具体到类型(接口或类)
- args标志符:args用于指定方法的参数,根据 方法参数个数 和 类型 去匹配构成切入点的连接点
- bean标志符:指定目标方法所在的bean的id或名称
- @annotation标志符:指定某个注解修饰的方法构成切入点,具体到指定注解修饰的方法。指定注解的包名.注解名,说明该注解修饰的方法构成切入点
- @within标志符: 指定某个注解修饰的类型中的方法构成切入点,具体到指定注解修饰的类型
//1. execution标志符: 指定方法执行作为连接点,具体到方法
@Pointcut("execution(* com.qdu.service.*.*(..))")
//2. within标志符: 指定连接点所在的类型,具体到类型(实现类)
@Pointcut("within(com.qdu.service.impl.StudentServiceImpl)")
//3. target标志符: 指定目标对象对应的类型,具体到类型(接口或类)
@Pointcut("target(com.qdu.service.StudentService)")
//4. args标志符:args用于指定方法的参数,根据方法参数个数和类型去匹配构成切入点的连接点
//在切入点表达式中可以使用&&、||、!逻辑运算符
@Pointcut("args(String) || args(Integer,Integer)")
//5. bean标志符: 指定目标方法所在的bean的id或名称
@Pointcut("bean(mathServiceImpl) || bean(teacherServiceImpl)")
//6. @annotation标志符: 指定某个注解修饰的方法构成切入点,具体到指定注解修饰的方法
//指定注解的包名.注解名,说明该注解修饰的方法构成切入点
@Pointcut("@annotation(com.qdu.aop.LogJoinPoint)")
//7. @within标志符: 指定某个注解修饰的类型中的方法构成切入点,具体到指定注解修饰的类型
@within(com.qdu.aop.LogJoinPoint) //表示LogJoinPoint注解修饰的类型中的方法构成切入点
1.2.3.4 练习
给出要应用切入点表达式的方法,试着写出切入点表达式:
- 所有方法
- 所有公开方法
- 所有以play开头的方法
- com.qdu.service.impl.StudentServiceImpl类型中的所有方法
- com.qdu.service.impl.StudentServiceImpl类型中带一个String类型参数的方法
- com.qdu.service.impl.StudentServiceImpl类型中带两个个String类型参数的方法
- com.qdu.service.StudentService类型中的所有方法
- com.qdu.service包下的所有类型中的所有方法
- com.qdu.service包和其子包下所有类型中的所有方法
以下是答案:
//1. 所有方法
@Pointcut("execution(* *(..))")
public void pointcut1() {
}
//2. 所有公开方法
@Pointcut("execution(public * *(..))")
public void pointcut2() {
}
//3. 所有以play开头的方法
@Pointcut("execution(* play*(..))")
public void pointcut3() {
}
//4. com.qdu.service.impl.StudentServiceImpl类型中的所有方法
@Pointcut("execution(* com.qdu.service.impl.StudentServiceImpl.*(..))")
public void pointcut4() {
}
//5. com.qdu.service.impl.StudentServiceImpl类型中带一个String类型参数的方法
@Pointcut("execution(* com.qdu.service.impl.StudentServiceImpl.*(String))")
public void pointcut5() {
}
//6. com.qdu.service.impl.StudentServiceImpl类型中带两个个String类型参数的方法
@Pointcut("execution(* com.qdu.service.impl.StudentServiceImpl.*(String, String))")
public void pointcut6() {
}
//7. com.qdu.service.StudentService类型中的所有方法
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pointcut7() {
}
//8. com.qdu.service包下的所有类型中的所有方法
@Pointcut("execution(* com.qdu.service.*.*(..))") //注意第二个.*
public void pointcut8() {
}
//9. com.qdu.service包和其子包下所有类型中的所有方法
@Pointcut("execution(* com.qdu.service..*.*(..))") //注意第一个是..*
public void pointcut9() {
}
2 AOP实现
方面/切面(Aspect)是将次要业务逻辑/次要关注点/横切关注点模块化为方面,在这里我们将日志功能做成一个方面
2.1 AOP配置
2.1.1 基于XML Schema的AOP(使用<aop:config>配置AOP)
在Spring的XML配置文件中使用<aop:aspectj-autoproxy />,这个标签的作用是使@AspectJ注解生效。
<aop:aspectj-autoproxy />
然后在com.qdu.aop包中创建一个LogAspect.class,这个类包含的是次要业务逻辑:日志功能。
@Aspect
public class LogAspect {}
如果要使用<aop:config>配置而不使用诸如@Aspect注解(纯使用xml)则略微麻烦。
首先方面对应的类应该注册为Spring管理的bean,才能将方面切入到需要的地方
<bean id="logAspect1" class="com.qdu.aop.LogAspect1" />
<bean id="logAspect2" class="com.qdu.aop.LogAspect2" />
在<aop:config>中:
- <aop:pointcut>:定义切入点,这样的切入点可以在多个<aop:aspect>中使用。
- <aop:aspect>:配置一个方面对应的类,ref属性指定方面类bean的id或name,order用于控制通知的执行顺序,值越小越先执行。注意:在aop:aspect标记内定义的切入点只能在该aop:aspect标记中使用
- <aop:before>:配置前置通知,method属性指定作为前置通知的方法的名称,pointcut属性用于指定切入点表达式,pointcut-ref属性用于指定引用的切入点的id
- <aop:after-returning>:配置抛出后通知,throwing属性指定一个参数名(可随便起,尽量有意义),这样可以抛出后通知对应的方法上添加一个该名称的参数,用于接收抛出的异常对象
- <aop:after>:用于配置最终通知/后置通知
- <aop:around>:用于配置环绕通知
<aop:config>
<aop:pointcut
expression="execution(* com.qdu.service.StudentService.*(..))"
id="pt1" />
<aop:aspect ref="logAspect1" order="2">
<!-- 在aop:aspect标记内定义的切入点只能在该aop:aspect标记中使用 -->
<aop:pointcut
expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />
<aop:before method="before1" pointcut-ref="pt1" />
<aop:before method="before2" pointcut-ref="pt1" />
<aop:after-returning method="afterReturning" pointcut-ref="pt2"
returning="returnValue" />
<aop:after-throwing method="afterThrowing" pointcut-ref="pt3" throwing="ex" />
<aop:after method="after" pointcut-ref="pt3"/>
<aop:around method="around" pointcut-ref="pt4"/>
</aop:aspect>
<aop:aspect ref="logAspect2" order="1">
<aop:before method="before3" pointcut-ref="pt1" />
<aop:before method="before4" pointcut-ref="pt1" />
</aop:aspect>
</aop:config>
2.1.2 @AspectJ风格的AOP(使用@Aspect+@Before等注解)
也可以在SpringConfig配置类中使用@EnableAspectJAutoProxy注解,该注解的作用也是启用对@AspectJ、@Before等注解的支持。而且开启包扫描时不要忘记扫描次要业务逻辑的包(这里是com.qdu.aop)
@Configuration
@ComponentScan({"com.qdu.service","com.qdu.aop"})
@EnableAspectJAutoProxy
public class SpringConfig {
}
在LogAspect.java中,除了用@Aspect修饰这个包含次要逻辑的类,也不要忘记@Component
@Aspect
@Component
public class LogAspect {}
2.1.3 使用Spring API实现Spring AOP
该方法我使用的不多,对此也并不是很理解,仅附上代码,日后待更
Spring的xml配置文件:
<!-- 1. 创建目标对象 -->
<bean id="studentService" class="com.qdu.service.impl.StudentServiceImpl" />
<!-- 2. 创建通知 -->
<bean id="beforeAdvice" class="com.qdu.aop.MyBeforeAdvice" />
<bean id="afterReturningAdvice" class="com.qdu.aop.MyAfterReturningAdvice" />
<!-- 通知器:一种用于指定何种通知应用于何种切入点的组件 -->
<!-- 2. 创建通知器和定义切入点,这里使用正则表达式切入点通知器,该类是一个方便类,允许使用正则表达式指定切入点,并指定在该切入点应用的通知 -->
<bean id="advisor1" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="beforeAdvice" />
<property name="pattern" value=".*" />
</bean>
<bean id="advisor2" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice" ref="afterReturningAdvice" />
<property name="pattern" value=".*play.*" />
</bean>
<!-- 3. 创建代理 -->
<!-- ProxyFactoryBean这个类用于指定代理对象如何生成,需要提供目标对象 、目标对象实现的接口、应用的通知器-->
<bean id="studentServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="studentService" />
<property name="proxyInterfaces" value="com.qdu.service.StudentService" />
<property name="interceptorNames">
<list>
<value>advisor1</value>
<value>advisor2</value>
</list>
</property>
</bean>
2.2 实现各种通知
通知(Advice)即方面包含的操作,在spring代码中,通知对应的是方法
通知有5种类型:
- 前置通知(Before Advice):是在目标方法执行之前执行的通知
- 返回后通知(After Returning Advice): 是目标方法正常执行返回后执行的通知
- 抛出后通知(After Throwing Advice):是目标方法执行发生异常,抛出异常后执行的通知
- 后置通知(After Advice):不管目标方法执行是否正常,方法执行完总要执行的通知
- 环绕通知(Around Advice):环绕一个方法执行的通知,是最强大的通知
2.2.1 基于XML
2.2.1.1 前置通知
<aop:config>
<aop:pointcut
expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />
<aop:aspect ref="logAspect1" order="2">
<aop:pointcut
expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />
<aop:before method="before1" pointcut-ref="pt1" />
<aop:before method="before2" pointcut-ref="pt1" />
</aop:aspect>
</aop:config>
LogAspect1.java:
public class LogAspect1 {
public void before1(JoinPoint point) {
System.out.println("..............前置通知1,目标方法:" + point.getSignature().getName());
}
public void before2(JoinPoint point) {
System.out.println("..............前置通知2");
}
}
2.2.1.2 返回后通知
returning属性用于指定一个参数名,这样可以在返回后通知对应的方法上添加一个该名称的参数,用于接收目标方法的返回值
<aop:config>
<aop:pointcut
expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />
<aop:aspect ref="logAspect1" order="2">
<aop:pointcut
expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />
<aop:after-returning method="afterReturning" pointcut-ref="pt2"
returning="returnValue" />
</aop:aspect>
</aop:config>
LogAspect1.java:
public class LogAspect1 {
public void afterReturning(JoinPoint point, Object returnValue) {
System.out.println("..............返回后通知,返回值:" + returnValue);
}
}
2.2.1.3 抛出后通知
throwing属性用于指定一个参数名,这样可以在抛出后通知对应的方法上添加一个该名称的参数,用于接收抛出的异常对象
<aop:config>
<aop:pointcut
expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />
<aop:aspect ref="logAspect1" order="2">
<aop:pointcut
expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />
<aop:after-throwing method="afterThrowing" pointcut-ref="pt3" throwing="ex" />
</aop:aspect>
</aop:config>
LogAspect1.java:
public class LogAspect1 {
public void afterThrowing(JoinPoint point, Throwable ex) {
System.out.println("..............抛出后通知,异常消息:" + ex.getMessage());
}
}
2.2.1.4 最终/后置通知
<aop:config>
<aop:pointcut
expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />
<aop:aspect ref="logAspect1" order="2">
<aop:pointcut
expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />
<aop:after method="after" pointcut-ref="pt3"/>
</aop:aspect>
</aop:config>
LogAspect1.java:
public class LogAspect1 {
public void after(JoinPoint point) {
System.out.println("..............最终通知");
}
}
2.2.1.5 环绕通知
<aop:config>
<aop:pointcut
expression="execution(* com.qdu.service.StudentService.*(..))" id="pt1" />
<aop:aspect ref="logAspect1" order="2">
<aop:pointcut
expression="execution(* com.qdu.service.MathService.add(..))" id="pt2" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.divide(..))" id="pt3" />
<aop:pointcut
expression="execution(* com.qdu.service.MathService.multiply(..))" id="pt4" />
<aop:around method="around" pointcut-ref="pt4"/>
</aop:aspect>
</aop:config>
LogAspect1.java:
public class LogAspect1 {
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("..............环绕通知");
return point.proceed();
}
}
2.2.2 基于Java代码与注解
不同于XML配置,使用Java代码配置则是更多地使用Java配置类和注解
首先给出Java配置类,主要是开启包扫描及启用AspectJ风格
@Configuration
@ComponentScan({"com.qdu.service","com.qdu.aop"})
@EnableAspectJAutoProxy
public class SpringConfig {
}
接下来就是使用AspectJ的切入点表达式来指定应用通知的切入点,通过注解完成
2.2.2.1 前置通知
@Before("execution(* com.qdu.service.StudentService.*(..))")
public void beforeAdvice1(JoinPoint point) {
System.out.println("~~~~~~~~~~~~~~~~~~~"+point.getSignature().getName()+"方法执行前");
}
这里添加了一个JoinPoint类型的参数,用于获取构成切入点的连接点的信息
这里我们给出一个实际的例子,此时我们有了一个com.qdu.service.MathService接口,并且有了该接口的实现类
我们对实现类中的add方法应用前置通知并获取方法中的一些信息:
@Before("execution(* com.qdu.service.MathService.*(..))")
public void beforeAdvice2(JoinPoint point) {
System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
+point.getSignature().getName()+"方法调用前");
System.out.println("目标方法名:"+point.getSignature().getName());
System.out.println("目标方法详细信息:"+point.getSignature().toLongString());
//getTarget()可以获取目标对象,如果在通知中需要操作目标对象,可获取该对象操作
System.out.println("目标对象:"+point.getTarget().getClass());
System.out.print("目标方法的参数:");
//getArgs()用于获取目标方法传入的实际参数,以一个Object[]返回
Object[] args=point.getArgs();
for(Object arg:args) {
System.out.print(arg+" ");
}
System.out.println();
}
输出如下图所示:
2.2.2.2 返回后通知
@AfterReturning("execution(* com.qdu.service.*.*(..))")
public void afterReturningAdvice(JoinPoint p) {
System.out.println("..................."+p.getSignature().getName()+"方法执行返回后");
}
@AfterReturning注解的returning属性用于指定一个方法参数名,该名称对应的参数用于接收目标方法的返回值。如果希望表示任何类型的返回值,请使用Object类型
@Pointcut("execution(* com.qdu.service..*(..))")
public void pointcut1() {
}
@AfterReturning(value = "pointcut1()", returning = "returnValue")
public void afterReturningAdvice(JoinPoint point, Object returnValue) {
System.out.println("----------------------------------------"
+ point.getSignature().getName()
+ "方法调用返回后,返回值:"+returnValue);
}
调用后运行结果如下:
当然,若方法执行出现异常则不执行返回后通知
2.2.2.3 抛出后通知
使用该类型的通知来收集异常信息。抛出后通知不会阻止异常的发生,只是在目标方法发生异常后做点事情。
一旦目标方法发生异常,会抛出异常对象,如果希望在抛出后通知中获取该异常对象,从而获取异常信息,可以通过throwing指定一个参数名,并在抛出后通知对应的方法上添加一个该名称的参数用于接收异常对象
如果指定异常类型是某个类型,则只有发生该类型异常的方法才会应用通知;如果希望能表示所有类型的异常,可以使用Exception或Throwable定义
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.qdu.service.MathService.*(..))")
public void pointcut1() {
}
@AfterThrowing(value = "pointcut1()",throwing="ex")
public void afterThrowingAdvice(JoinPoint point,Throwable ex) {
System.out.println("----------------------------------------"
+ point.getSignature().getName() + "方法抛出异常后,异常消息:"
+ex.getMessage());
}
}
运行一个会抛出异常的方法,如下图所示:
2.2.2.4 后置通知
不管目标方法正常执行返回还是抛出异常都会执行的通知
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.qdu.service.MathService.*(..))")
public void pointcut1() {
}
@After("pointcut1()")
public void afterAdvice(JoinPoint point) {
System.out.println("----------------------------------------"
+ point.getSignature().getName()
+ "方法执行后,参数值:"
+point.getArgs()[0]+"和"+point.getArgs()[1]);
}
}
执行结果:
2.2.2.5 环绕通知
1. 环绕通知是包围一个方法的通知,它可以控制方法的执行
默认情况下,环绕通知会拦截目标方法的执行,也就是导致目标方法不会执行;如果希望能够获取连接点的信息和控制目标方法的执行,可以添加一个ProceedingJoinPoint类型的参数
@Aspect
@Component
public class LogAspect1 {
@Pointcut("execution(* com.qdu.service.*.playGames(..))")
public void pointcut1() {
}
@Around("pointcut1()")
public void aroundAdvice(ProceedingJoinPoint point) throws Throwable{
System.out.println("--------------------环绕通知,调用目标方法前");
//调用ProceedingJoinPoint的proceed()方法让目标方法执行
//这里获取连接点,也就是目标方法的参数,返回的是一个Object[]
//[0]表示获取方法的第一个参数,实际的参数是String类型,所以这里转成字符串
String gameName=point.getArgs()[0].toString();
//可以根据一个条件决定是否让目标方法执行
if(gameName.contains("绝地")||gameName.contains("求生")) {
System.out.println("打什么绝地求生,好好学习~~~");
} else {
point.proceed();
}
System.out.println("~~~~~~~~~~~~~~~~~~~~环绕通知,调用目标方法后");
}
}
运行结果如下:
2. 环绕通知不仅可以控制目标方法的执行,还可以控制目标方法的返回值
如果环绕通知返回类型为void,会导致有返回值的目标方法的返回值为null。可以在环绕通知中将目标方法的值返回,让目标方法的值得以正常返回
@Aspect
@Component
public class LogAspect2 {
@Pointcut("execution(* com.qdu.service.MathService.*(..))")
public void pointcut1() {
}
@Around("pointcut1()")
public Object aroundAdvice(ProceedingJoinPoint point) throws Throwable{
System.out.println("--------------------环绕通知,目标方法:"
+ point.getSignature().getName());
//proceed()方法调用会导致目标方法会调用,返回的值就是目标方法的返回值
Object returnValue=point.proceed();
//环绕通知这里返回的值就是最终返回的值
return returnValue;
}
}
运行结果如下:
看过以上两个例子,我们发现环绕通知可以将以上四种通知结合起来,也可以替代以上四种任何一种通知(但是尽量使用最合适的通知)
下面这个例子中,我们用环绕通知将以上四种通知集成到环绕通知中:
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.qdu.service.MathService.*(..))")
public void pointcut1() {
}
@Around("pointcut1()")
public Object aroundAdvice(ProceedingJoinPoint point) {
Object result=null;
try {
System.out.println("--------这是目标方法执行前执行的代码");
result=point.proceed();
System.out.println("~~~~~~~~这是目标方法正常执行返回后会执行的代码");
} catch (Throwable e) {
System.out.println("^^^^^^^^这是目标方法执行抛出异常后执行的代码");
} finally {
System.out.println("********这是目标方法不管是否出现异常,都会执行的代码");
}
return result;
}
}
运行10/2,发现该环绕通知实现了前置、返回后、后置通知的功能:
运行10/0,发现该环绕通知实现了前置、抛出后、后置通知的功能:
2.3 多个通知的执行顺序
前面我们提到,如果多个同样的通知应用到同一个目标方法,执行顺序是未知的,但是可以通过使用@Order注解来控制执行顺序,@Order里的值越小表示顺序越靠前
假如我们有两个LogAspect,LogAspect1的Order值为2,有beforeAdvice1、beforeAdvice2、环绕通知、afterReturningAdvice1、afterReturningAdvice2;LogAspect2的Order值为1,有beforeAdvice3、beforeAdvice4;每个LogAspect都只有一个切入点,且该切入点运用到多个通知
@Aspect
@Component
@Order(2)
public class LogAspect1 {
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pointcut1() {
}
@Before("pointcut1()")
public void doBefore1() {
System.out.println("..........beforeAdvice1..........");
}
@Before("pointcut1()")
public void doBefore2() {
System.out.println("..........beforeAdvice2..........");
}
@AfterReturning("pointcut1()")
public void doAfterReturning1() {
System.out.println("..........afterReturningAdvice1..........");
}
@AfterReturning("pointcut1()")
public void doAfterReturning2() {
System.out.println("..........afterReturningAdvice2..........");
}
@Around(value = "pointcut1()")
public void aroundAdvice(ProceedingJoinPoint point) throws Throwable {
System.out.println("..........环绕通知中调用目标方法前的代码..........");
point.proceed();
System.out.println("..........环绕通知中调用目标方法后的代码..........");
}
}
@Aspect
@Component
@Order(1) //值越小表示顺序越靠前
public class LogAspect2 {
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pointcut1() {
}
@Before("pointcut1()")
public void doBefore3() {
System.out.println("..........beforeAdvice3..........");
}
@Before("pointcut1()")
public void doBefore4() {
System.out.println("..........beforeAdvice4..........");
}
}
LogAspect2的Order值小,所以LogAspect2中的通知应该优先执行。所以在4+1个前置通知中,LogAspect2中的beforeAdvice3、beforeAdvice4应优先执行,然后再执行LogAspect1中的
执行结果如下:
2.4 AOP小练习
- 至少使用一次“切入点签名+切入点表达式”的写法定义一个该类中要用的切入点
- 定义一个方法,作为一个前置通知,包含目标方法执行前要执行的代码,通知应用到StudentService的所有方法
- 定义一个方法,作为一个返回后通知,包含目标方法执行成功返回后要执行的代码,通知应用到com.qdu.service包和子包内的所有类型的所有方法,但是排除divide2方法,并打印方法返回值
- 定义一个方法,作为一个抛出后通知,包含目标方法发生异常执行的方法,通知应用到MathService接口的divide开头的方法,并打印error级别的日志信息,要求打印异常消息
- 定义一个方法,作为一个环绕通知,应用到名为divide2方法上,用于集中处理异常,如果发生异常,打印error级别的日志信息和异常消息。请确保如果目标方法有返回值,返回值会正常返回
1 + 2.至少使用一次“切入点签名+切入点表达式”的写法定义一个该类中要用的切入点
定义一个方法,作为一个前置通知,包含目标方法执行前要执行的代码,通知应用到StudentService的所有方法
private static Logger logger = LoggerFactory.getLogger(LogAspect1.class);
@Pointcut("execution(* com.qdu.service.StudentService.*(..))")
public void pt1() {
}
@Before("pt1")
public void beforeAdvice(JoinPoint point) {
logger.debug("~~" + point.getSignature().getName() + "方法调用前");
}
3. 定义一个方法,作为一个返回后通知,包含目标方法执行成功返回后要执行的代码,通知应用到com.qdu.service包和子包内的所有类型的所有方法,但是排除divide2方法,并打印方法返回值
@AfterReturning(
value = "within(com.qdu.service..*) && !execution(* divide2(..))",
returning = "rv"
)
public void afterReturningAdvice(JoinPoint point, Object rv) {
logger.debug("**" + point.getSignature().getName() + "方法正常执行返回后,方法返回值" + rv);
}
4. 定义一个方法,作为一个抛出后通知,包含目标方法发生异常执行的方法,通知应用到MathService接口的divide开头的方法,并打印error级别的日志信息,要求打印异常消息
@AfterThrowing(
value = "execution(* com.qdu.service.MathService.divide*(..))",
throwing = "ex"
)
public void afterThrowingAdvice(JoinPoint point, Throwable ex) {
logger.debug("--" + point.getSignature().getName() + "方法发生异常后,异常消息:" + ex.getMessage());
}
5. 定义一个方法,作为一个环绕通知,应用到名为divide2方法上,用于集中处理异常,如果发生异常,打印error级别的日志信息和异常消息。请确保如果目标方法有返回值,返回值会正常返回
@Around("execution(* divide2(..))")
public Object aroundAdvice(ProceedingJoinPoint point) {
Object returnValue = null;
try {
returnValue = point.proceed();
} catch(Throwable e) {
logger.error("发生异常,目标方法:" + point.getSignature().getName() + ",异常信息:" + e.getMessage());
}
return returnValue
}
运行结果如下: