这两天读了《Spring实战》第四章,总结一下。
其中章节4.3.4的”通过注解引入新功能”,以及章节4.5的”注入AspectJ切面”这本书上讲的不是很清楚,而且也有些复杂,也并不常用,有机会单独拿出来学习一下。
面向切面编程
在程序中,如日志、安全、缓存、事务管理等不是业务本身该做的,但是总是和很多业务逻辑一起出现,这些事件对业务逻辑来说是被动的。在业务程序中被动地做这些事的位置称为横切关注点,AOP所做的事情就是将这些横切关注点和业务逻辑分离。
重用这些通用功能的常见做法是继承或委托,AOP也是提供了另外的思路,通过声明的方式定义通用功能在业务逻辑的什么位置使用,当横切关注点被模块化成特殊的类时,该类称为切面。
想象一下空间中的多个点可以用曲面来拟合,多个横切关注点可以被模块化成切面。切面也就是整合这些通用功能的服务模块。
AOP术语
这些东西太抽象了,大致理解一下,知道怎么用就好了。
通知(Advice)
切面的工作被称为通知,通知定义了切面要完成的具体事情和何时使用。
Spring的切面有5种类型的通知:
- 前置通知(Before):在目标方法被调用前调用
- 后置通知(After):在目标方法完成之后调用,不关心方法输出了什么
- 返回通知(After-returning):在目标方法成功执行之后调用
- 异常通知(After-throwing):在目标方法抛出异常之后调用
- 环绕通知(Around):包裹被通知的方法,在调用的前后做事情
连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、修改一个字段时等等。切面中的通知代码通过这些点插入到业务的流程中,从而添加新的行为。
切点(Poincut)
一个切面不需要通知应用的所有连接点,切点的定义会匹配通知所要织入的一个或多个连接点,有助于缩小切面所通知连接点的范围。
切面(Aspect)
切面是通知和切点的结合——它是什么,在何时和何处完成其功能。
引入(Introduction)
引入允许向现有的类添加新的方法或属性,可以在无需修改现有类的情况下让它们具有新的行为和状态。
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。织入可以在编译期、类加载期、运行期。目前讨论的都是运行期的织入,Spring AOP会为目标对象动态地创建一个代理对象。
Spring对AOP的支持
Spring提供了4种类型的AOP支持:
- 基于代理的Spring AOP(太老了不看)
- 纯POJO切面
@Aspect
注解驱动的切面- 注入式AspectJ切面
Spring中
Spring的@Aspect
注解借鉴了AspectJ,但本质上仍是基于代理的AOP。总之在Spring本身的AOP中使用注解或XML来实现AOP。
Spring在运行期间把切面织入到Spring管理的bean中,代理类封装了目标类,并拦截被通知方法的调用,在切面逻辑的包围下,在适当的时机把调用转发给真正的目标bean。
因为Spring基于动态代理,所以只支持方法级别的连接点,但已经可以满足绝大多数功能,需要更强大的功能时可以选择使用AspectJ。
AspectJ中
在AspectJ中,可以通过注解或特有的扩展语言获得更强大的细粒度的控制,以及更丰富的AOP工具集。
通过切点选择连接点
Spring借助AspectJ的切点表达式语言定义Spring切面,但它只支持AspectJ切点指示器的一部分功能。可用的有:
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@arg() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target() | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解标注的类型(当使用Spring AOP时,方法定义在由Spring注解所标注的类里) |
@annotation() | 限制匹配带有指定注解的连接点 |
只有execution()
是执行匹配的,其它的指示器都是用来限制匹配的。
编写切点表达式
业务逻辑的抽象接口,就按照书上的例子,提供一个可以做”表演”的接口:
//表演接口
public interface Performance {
//表演方法
void perform() throws Exception;
}
要让这个接口的实现类所实现的perform()方法成为切点,要使用切点表达式语言:
"execution(* org.aop.Performance.perform(..))"
其中execution()
表示在方法执行时触发,里面紧跟的一个*
表示返回任意类型,后面是方法所属的类(这里是接口),然后是方法的名称,方法中..
表示选择任意重载的该方法,而不关心其参数表。
该接口可能有多个实现类,如果要加个条件,限制只选择某个指定包下的所有该接口的实现类,则要这样写:
"execution(* org.aop.Performance.perform(..)) && within(org.aop.imp.*)"
使用&&
让它和within()
指示器之间形成合取关系,当在XML中使用时,因为&
符有特殊含义,可以使用相应的英文来代替:
关系符号 | 在XML中使用英文代替 |
---|---|
&& | and |
|| | or |
! | not |
用
bean()
指示器则可以明确指明该接口的实现bean的id:
"execution(* org.aop.Performance.perform(..)) && bean(pfmcimp)"
这个切点表达式在XML中则要写成(书上加单引号是错的):
"execution(* org.aop.Performance.perform(..)) and bean(pfmcimp)"
使用非操作排除指定id的bean,为其他实现bean提供应用通知:
"execution(* org.aop.Performance.perform(..)) && !bean(pfmcimp)"
使用注解配置切面
四种简单的通知
@Before
、@AfterReturning
、@AfterThrowing
、@After
是四种最常用的最简单的通知。
切面类
package org.aop;
import org.aspectj.lang.annotation.*;
@Aspect//标注该类为切面类
public class Audience {
//定义一个可重用的切点表达式,可供各个通知方法引用
@Pointcut("execution(* org.aop.Performance.perform(..)) && bean(pfmcimp)")
public void perform() {
//该方法不需要内容,只是作为一个标识,供@Pointcut注解依附
}
//在目标方法执行前
@Before("perform()")//这里不再直接写切点表达式,而是引用切点方法
public void silenceCellPhones() {
System.out.println("[Before]表演前,把手机静音");
}
//在目标方法正常返回后
@AfterReturning("perform()")
public void applause() {
System.out.println("[AfterReturning]表演正常结束(return返回)后,鼓掌");
}
//在目标方法抛出异常后
@AfterThrowing("perform()")
public void demandRefund() {
System.out.println("[AfterThrowing]表演没有顺利完成(抛出异常),要求退款");
}
//在目标方法执行后(不论是正常返回还是抛出异常)
@After("perform()")
public void evaluate() {
System.out.println("[After]表演结束了,观众在心里对表演进行评估");
}
}
配置类
package org.sb;
import org.aop.Audience;
import org.aop.Performance;
import org.aop.imp.PerformanceImp;
import org.springframework.context.annotation.*;
@Configuration//标注该类为JavaConfig配置类
@EnableAspectJAutoProxy//启动自动代理功能
@ImportResource(value = {"classpath:ApplicationContext.xml"})//引入其它配置类或配置文件
@ComponentScan(basePackageClasses = {Audience.class})//自动扫描指定类或接口所在的基本包
public class SbConfig {
@Bean
public Audience audience() {
//观众的行为类(切面配置类)
return new Audience();
}
@Bean
public Performance pfmcimp() {
//Performance(表演接口)的一个实现类对象,将被代理
return new PerformanceImp();
}
}
特别注意在JavaConfig中使用@EnableAspectJAutoProxy
注解开启自动代理功能,如果是在XML中,应使用:
<aop:aspectj-autoproxy/>
不论哪种配置方式,AspectJ自动代理都会为使用@Aspect
注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。
实现bean
在实现bean所实现的目标方法中故意抛出一个异常:
package org.aop.imp;
import org.aop.Performance;
import org.springframework.stereotype.Component;
@Component//标注该类是组件类,扫描后将成bean交给Spring容器管理
public class PerformanceImp implements Performance {
@Override
public void perform() throws Exception {
System.out.println("LZH表演了自己是怎么吃饭的");
throw new Exception("666");//故意抛出一个异常看一下
}
}
场景类
使用者只需调用目标方法,而无需关心切面上的通知:
package org.aop;
import org.sb.SbConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
//从JavaConfig配置类中加载Spring的ApplicationContext上下文
ApplicationContext context = new AnnotationConfigApplicationContext(SbConfig.class);
//获得目标方法所在接口的一个实现bean
Performance performance = (Performance) context.getBean("pfmcimp");
try {
//调用其中的实现方法,该方法已经被定义成了[切点],并被加上了[切面]
//代理类封装了目标bean,拦截方法调用,执行切面逻辑
performance.perform();
} catch (Exception e) {
//执行过程中可能抛出的异常
System.err.println("处理目标方法中抛出的异常");//System.err输出不带缓存,有时会跑到System.out的前面
}
}
}
输出
如果不用System.err
而还是用System.out
的话,输出顺序就能保证了。
环绕通知
环绕通知是更强大的一种通知类型,顾名思义,就是将目标方法包裹起来调用。环绕通知中ProceedingJoinPoint
类型的参数用来执行目标方法,环绕通知必须有返回值,返回值即为目标方法的返回值。
切面类
修改一下切面类,只需要一个环绕通知就可以完成前面四种通知的功能。
package org.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect//标注该类为切面类
public class Audience {
//定义一个可重用的切点表达式,可供各个通知方法引用
@Pointcut("execution(* org.aop.Performance.perform(..)) && bean(pfmcimp)")
public void perform() {
//该方法不需要内容,只是作为一个标识,供@Pointcut注解依附
}
//环绕在目标方法上
@Around("perform()")
public Object watchPerformance(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
System.out.println("[Before]表演前,把手机静音");//@Before
result = joinPoint.proceed();//调用目标方法
System.out.println("[AfterReturning]表演正常结束(return返回)后,鼓掌");//@AfterReturning
} catch (Throwable throwable) {
System.out.println("[AfterThrowing]表演没有顺利完成(抛出异常),要求退款");//@AfterThrowing
}
System.out.println("[After]表演结束了,观众在心里对表演进行评估");//@After
return result;//返回目标方法的执行结果
}
}
输出
注意和直接使用四种通知时的区别,After
通知的位置不太一样。另外这种方式在内部处理了异常,在异常处理内执行操作,而AfterThrowing
实际是在抛出异常时执行的操作,所以前面那种是能在场景类中捕获异常的,这种方式则因为处理过了不能再捕获。
在通知中访问目标方法的参数
如果目标方法带有参数,如在前面例子的接口和实现类方法中加入一个String类型的name参数用来指明是谁在表演,在场景类调用时传入”刘知昊”。
切面类
package org.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect//标注该类为切面类
public class Audience {
//指明要String类型的参数,在这里给参数一个别名performer
@Pointcut("execution(* org.aop.Performance.perform(String)) && bean(pfmcimp) && args(performer)")
//方法中传入String类型的这个参数
public void perform(String performer) {
}
//环绕在目标方法上,写明参数
@Around("perform(performer)")
//目标方法上的参数如果需要通知使用,则从通知方法传入即可使用
public Object watchPerformance(ProceedingJoinPoint joinPoint, String performer) {
Object result = null;
try {
System.out.println("[Before]"+performer+"表演前,把手机静音");
//在用此方法调用目标方法时,并不需要传入参数,参数仅仅是给通知用的
result = joinPoint.proceed();
System.out.println("[AfterReturning]表演正常结束(return返回)后,鼓掌");
} catch (Throwable throwable) {
System.out.println("[AfterThrowing]"+performer+"表演没有顺利完成(抛出异常),要求退款");
}
System.out.println("[After]表演结束了,观众在心里对表演进行评估");
return result;
}
}
特别注意,参数传进来仅仅考虑到通知可能会用到,目标方法本身是用不着重新传一遍的,这里千万不要搞混了。这体现在环绕通知里的joinPoint.proceed()
不需要传这个参数。
输出
使用XML配置切面
为什么要使用XML配置
注解和自动代理提供了最简的配置AOP的方式,但它有个明显的劣势:必须有办法为通知类添加注解。也就是必须要有源码,如果不能获得源码的话就不能用这种方式了(因为没法去添加注解)。使用XML创建切面则避免了这个问题。
注意,使用XML配置时就相当于拿不到切面类的源码,切面类的所有与Spring AOP相关的注解全部去掉。
AOP配置元素
使用Spring的AOP配置元素能彻底以非侵入性的方式声明切面:
AOP配置元素 | 功能 |
---|---|
<aop:config> | 顶层的AOP配置元素,大多元素都需包含在这个元素内 |
<aop:advisor> | 定义AOP通知器 |
<aop:aspect> | 定义一个切面,通知需要定义在切面中 |
<aop:pointcut> | 定义一个切点,指明切点表达式 |
<aop:aspectj-autoproxy/> | 自动代理,启用@Aspect 注解驱动的切面 |
在切面内:
切面内的元素 | 功能 |
---|---|
<aop:after> | 后置通知 |
<aop:after-returning> | 返回通知 |
<aop:after-throwing> | 异常通知 |
<aop:around> | 环绕通知 |
<aop:before> | 前置通知 |
<aop:declare-parents> | 以透明的方式为被通知的对象引入额外的接口 |
四种简单的通知
<!--切面bean-->
<bean class="org.aop.Audience" id="audience"/>
<!--配置AOP-->
<aop:config>
<!--这个定义[切点]的元素完全可以放在切面外面,这样所有切面都能用它-->
<aop:pointcut id="perform" expression="execution(* org.aop.Performance.perform(String)) and bean(pfmcimp) and args(performer)"/>
<!--定义[切面],引用切面bean-->
<aop:aspect ref="audience">
<!--定义[通知],引用前面定义的[切点],通知参数用arg-names属性指定-->
<aop:before method="silenceCellPhones" pointcut-ref="perform" arg-names="performer"/>
<aop:after-returning method="applause" pointcut-ref="perform" arg-names="performer"/>
<aop:after-throwing method="demandRefund" pointcut-ref="perform" arg-names="performer"/>
<aop:after method="evaluate" pointcut-ref="perform" arg-names="performer"/>
</aop:aspect>
</aop:config>
输出:
环绕通知
只要把里面的前置、后置通知都换成环绕通知,别的不用改。
<!--切面bean-->
<bean class="org.aop.Audience" id="audience"/>
<!--配置AOP-->
<aop:config>
<!--这个定义[切点]的元素完全可以放在切面外面,这样所有切面都能用它-->
<aop:pointcut id="perform" expression="execution(* org.aop.Performance.perform(String)) and bean(pfmcimp) and args(performer)"/>
<!--定义[切面],引用切面bean-->
<aop:aspect ref="audience">
<!--定义[通知],引用前面定义的[切点],通知参数用arg-names属性指定-->
<aop:around method="watchPerformance" pointcut-ref="perform" arg-names="joinPoint,performer"/>
</aop:aspect>
</aop:config>
输出: