AOP面向切面
概念
1、什么是面向切面编程?
切面能帮助我们模块化关注点。简而言之,横切关注点可以被描述为影响应用多处的功能。
项目开发过程中有些功能需要在多个模块中使用,比如:安全,事务等,那么就可以将安全和事务作为一个模块贯穿所有用到他们的模块中,类似一个切面,那么不需要每个模块自己单独实现。
横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。
好处:
首先现在每个关注点都集中于一个地方,而不是分散在多处代码中;
其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。
2、AOP术语
通知(advice):在AOP术语中,切面的工作被称为通知。通知定义了切面是什么以及何时使用。
Spring切面可以应用5中类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
连接点(Join Point):连接点是应用执行工程中能够插入切面的一个点。这个点可以是调用方法时,抛出异常时,修改字段时。切面可以利用这些点插入到应用的正常流程中,添加新的行为。
切点(Pointcot):个人理解切点就是连接点在切面上的位置。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
切面(Aspect):切面是通知和切点的结合
引入(Introduction):引入允许我们想现有的类添加新方法或者属性
织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。
在目标对象的生命周期里有多个点可以进行织入:
- 编译器:切面在目标类编译是被织入,这种方式需要特殊的编译器。AspectJ的织入编译器就是一这种方式织入切面的
- 类加载器:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入就支持以这种方式织入切面。
- 运行期:切面在应用运行的某个时期被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态的创建一个代理对象。SpringAOP就是以这用方式织入切面的。
Spring对AOP的支持
Spring提供了4中类型的AOP支持:
- 基于代理的经典SpringAOP;
- 纯POJO切面;
- @AspectJ注解驱动的切面;
- 注入式AspectJ切面(适用于Spring各版本)
前面三种都是SpringAOP实现的变体,SpringAOP构建在动态代理基础上,因此,Spring对AOP的支持局限于方法拦截。
通过切点来选择连接点
Spring仅支持AspectJ切点指示器的一个子集,如下:
AspectJ指 示器 | 描 述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象, 这些对象对应的类要具有指定类 型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时, 方 法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
我们发现只有execution指示器是实际执行匹配的,其他指示器都是用于限制匹配的,这说明execution指示器是我们在编写切点定义时最主要使用的指示器。
首先创建一个主题,比如表演,可以有舞台剧表演,戏剧表演,歌舞表演等
public interface Performance {
void perform();
}
execution的使用:
其中类名是完全限定名。
execution也可以与其他限制指示器结合使用
连接方式可以是&&(and )或者 || (or ) 或者 not(!)
使用注解创建切面
@AspectJ 支持三种通配符:
* :匹配任意字符,但它只能匹配上下文中的一个元素。
… :匹配任意字符,可以匹配上下文中的多个元素,参数的代表。
+ :匹配指定类的所有类,必须跟在类后面,如:com.tz.aspectj.IUserService+代表继承和扩展指定类的所有类,同时包括指定类本身。
Spring使用AspectJ注解来声明通知方法
注 解 | 通 知 |
---|---|
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用之前执行 |
那么创建一个观众类
//定义一个切面
@Aspect
public class Audience {
//执行perform方法之前,即表演之前观众就坐
@Before("execution(* com.cxd.springaop.service.Performance.perform(..))")
public void silenceCellPhones(){
System.out.println("表演前观众就坐");
}
//执行perform方法之前,即表演之前观众手机调静音
@Before("execution(* com.cxd.springaop.service.Performance.perform(..))")
public void takeSeats(){
System.out.println("表演前观众手机调成静音");
}
//执行perform方法调用后,即表演成功观众喝彩
@AfterReturning("execution(* com.cxd.springaop.service.Performance.perform(..))")
public void applause(){
System.out.println("表演精彩观众喝彩");
}
//执行perform方法调用异常时,即表演失败观众要求退票
@AfterThrowing("execution(* com.cxd.springaop.service.Performance.perform(..))")
public void demandRefund(){
System.out.println("表演出状况观众要求退票");
}
}
观众这些反应是基于同一个切点,所以这些切点可以整合,那么可以使用@Pointcut注解,如下:
@Aspect
public class Audience {
/**
*定义切点
*/
@Pointcut("execution(* com.cxd.springaop.service.Performance.perform(..))")
public void performance(){}
@Before("performance()")
public void silenceCellPhones(){
System.out.println("表演前观众就坐");
}
@Before("performance()")
public void takeSeats(){
System.out.println("表演前观众手机调成静音");
}
@AfterReturning("performance()")
public void applause(){
System.out.println("表演精彩观众喝彩");
}
@AfterThrowing("performance()")
public void demandRefund(){
System.out.println("表演出状况观众要求退票");
}
}
创建一个Performance的实现类
public class SingPerformanceImpl implements Performance {
public void perform() {
System.out.println("歌唱表演");
}
}
若使用JavaConfig的话,通过配置类装配bean,并且使用 @EnableAspectJAutoProxy 注解启用自动代理功能。
@Configuration
@EnableAspectJAutoProxy
public class JavaConfig {
@Bean("singPerformanceImpl")
public SingPerformanceImpl getSingPerformance(){
return new SingPerformanceImpl();
}
@Bean("audience")
public Audience audience(){
return new Audience();
}
}
测试类如下:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ProxyConfig.class)
public class AopTest {
/**此处必须注入的是接口,若注入具体实现类会报错,类似Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'singPerformanceImpl' is expected to be of type 'com.cxd.springaop.service.impl.SingPerformanceImpl' but was actually of type 'com.sun.proxy.$Proxy26'
*/
@Autowired
private Performance singPerformanceImpl;
@Test
public void test(){
singPerformanceImpl.perform();
}
结果:
表演前观众就坐
表演前观众手机调成静音
歌唱表演
表演精彩观众喝彩
创建环绕通知
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。
比如上面关于表演的例子,若使用环绕通知如下:
@Aspect
public class AroundAudience {
/**
*定义切点
*/
@Pointcut("execution(* com.cxd.springaop.service.Performance.perform(..))")
public void performance(){}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint joinPoint){
try {
System.out.println("表演前观众就坐");
System.out.println("表演前观众手机调成静音");
joinPoint.proceed();
System.out.println("表演精彩观众喝彩");
}catch (Throwable e){
System.out.println("表演出状况观众要求退票");
}
}
}
这里使用到了 ProceedingJoinPoint 作为参数,这个对象必须要有,因为要在通知中用它来调用被通知的方法。若没有调用proceed()这个方法,那么就会阻塞对被通知方法的调用,当然如果场景需要可以这么干,比如调用之前出现状况了,例如表演之前下暴雨了没法表演了,那么就不再调用被通知的方法。
处理通知带参数
上面我们举例被通知的方法没有参数,那么我们看看如果有参数该怎么处理。
@Aspect
public class AudienceNum {
@Pointcut("execution(* com.cxd.springaop.service.Performance.audienceNumber(int)) && args(audienceNum)")
public void performance(int audienceNum){}
@Around("performance(audienceNum)")
public void watchPerformance(ProceedingJoinPoint joinPoint,int audienceNum){
try {
System.out.println("表演前观众就坐");
System.out.println("表演前观众手机调成静音");
System.out.println("清点观众人数");
if (audienceNum >= 100){
System.out.println("增加座位");
}
joinPoint.proceed();
System.out.println("表演精彩观众喝彩");
}catch (Throwable e){
System.out.println("表演出状况观众要求退票");
}
}
}