AOP是Aspect Oriented Programming的缩写,意思是:面向切面编程,它是经过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。java
能够认为AOP是对OOP(Object Oriented Programming 面向对象编程)的补充,主要使用在日志记录,性能统计,安全控制等场景,使用AOP可使得业务逻辑各部分之间的耦合度下降,只专一于各自的业务逻辑实现,从而提升程序的可读性及维护性。git
好比,咱们须要记录项目中全部对外接口的入参和出参,以便出现问题时定位缘由,在每个对外接口的代码中添加代码记录入参和出参固然也能够达到目的,可是这种硬编码的方式很是不友好,也不够灵活,并且记录日志自己和接口要实现的核心功能没有任何关系。github
此时,咱们能够将记录日志的功能定义到1个切面中,而后经过声明的方式定义要在什么时候何地使用这个切面,而不用修改任何1个外部接口。正则表达式
在讲解具体的实现方式以前,咱们先了解几个AOP中的术语。spring
在AOP术语中,切面要完成的工做被称为通知,通知定义了切面是什么以及什么时候使用。编程
Spring切面有5种类型的通知,分别是:安全
链接点是在应用执行过程当中可以插入切面的一个点,这个点能够是调用方法时、抛出异常时、修改某个字段时。微信
切点是为了缩小切面所通知的链接点的范围,即切面在何处执行。咱们一般使用明确的类和方法名称,或者利用正则表达式定义所匹配的类和方法名称来指定切点。app
切面是通知和切点的结合。通知和切点共同定义了切面的所有内容:它是什么,在什么时候和何处完成其功能。ide
引入容许咱们在不修改现有类的基础上,向现有类添加新方法或属性。
织入是把切面应用到目标对象并建立新的代理对象的过程。
切面在指定的链接点被织入到目标对象中,在目标对象的生命周期里,有如下几个点能够进行织入:
Spring AOP构建在动态代理之上,也就是说,Spring运行时会为目标对象动态建立代理对象。
代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。
当代理类拦截到方法调用时,在调用目标bean方法以前,会执行切面逻辑。
经过在代理类中包裹切面,Spring在运行期把切面织入到Spring 管理的bean中,也就是说,直到应用须要被代理的bean时,Spring才会建立代理对象。
由于Spring运行时才建立代理对象,因此咱们不须要特殊的编译器来织入Spring AOP切面。
Spring只支持方法级别的链接点,若是须要字段级别或者构造器级别的链接点,能够利用AspectJ来补充Spring AOP的功能。
假设咱们有个现场表演的接口Performance和它的实现类SleepNoMore:
package chapter04.concert;
/** * 现场表演,如舞台剧,电影,音乐会 */
public interface Performance {
void perform();
}
复制代码
package chapter04.concert;
import org.springframework.stereotype.Component;
/** * 戏剧:《不眠之夜Sleep No More》 */
@Component
public class SleepNoMore implements Performance {
@Override
public void perform() {
System.out.println("戏剧《不眠之夜Sleep No More》");
}
}
复制代码
既然是演出,就须要观众,假设咱们的需求是:在看演出以前,观众先入座并将手机调整至静音,在观看演出以后观众鼓掌,若是演出失败观众退票,咱们固然能够把这些逻辑写在上面的perform()方法中,但不推荐这么作,由于这些逻辑理论上和演出的核心无关,就算观众不将手机调整至静音或者看完演出不鼓掌,都不影响演出的进行。
针对这个需求,咱们可使用AOP来实现。
首先,在pom.xml文件中添加以下依赖:
<!--spring aop支持-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.8.RELEASE</version>
</dependency>
<!--aspectj支持-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
复制代码
而后,定义一个观众的切面以下:
package chapter04.concert;
import org.aspectj.lang.annotation.Aspect;
/** * 观众 * 使用@Aspect注解定义为切面 */
@Aspect
public class Audience {
}
复制代码
注意事项:
@Aspect
注解代表Audience类是一个切面。
在Audience切面中定义前置通知以下所示:
/** * 表演以前,观众就座 */
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("Taking seats");
}
/** * 表演以前,将手机调至静音 */
@Before("execution(* chapter04.concert.Performance.perform(..))")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
复制代码
这里的重点代码是@Before("execution(* chapter04.concert.Performance.perform(..))")
,它定义了1个前置通知,其中execution(* chapter04.concert.Performance.perform(..))
被称为AspectJ切点表达式,每一部分的讲解以下:
在Audience切面中定义后置通知以下所示:
/** * 表演结束,无论表演成功或者失败 */
@After("execution(* chapter04.concert.Performance.perform(..))")
public void finish() {
System.out.println("perform finish");
}
复制代码
注意事项:@After注解用来定义后置通知,通知方法会在目标方法返回或者抛出异常后调用
在Audience切面中定义返回通知以下所示:
/** * 表演以后,鼓掌 */
@AfterReturning("execution(* chapter04.concert.Performance.perform(..))")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
复制代码
注意事项:@AfterReturning注解用来定义返回通知,通知方法会在目标方法返回后调用
在Audience切面中定义异常通知以下所示:
/** * 表演失败以后,观众要求退款 */
@AfterThrowing("execution(* chapter04.concert.Performance.perform(..))")
public void demandRefund() {
System.out.println("Demanding a refund");
}
复制代码
注意事项:@AfterThrowing注解用来定义异常通知,通知方法会在目标方法抛出异常后调用
细心的你可能会发现,咱们上面定义的5个切点中,切点表达式都是同样的,这显然是很差的,好在咱们可使用@Pointcut
注解来定义可重复使用的切点表达式:
/** * 可复用的切点 */
@Pointcut("execution(* chapter04.concert.Performance.perform(..))")
public void perform() {
}
复制代码
而后以前定义的5个切点均可以引用这个切点表达式:
/** * 表演以前,观众就座 */
@Before("perform()")
public void takeSeats() {
System.out.println("Taking seats");
}
/** * 表演以前,将手机调至静音 */
@Before("perform()")
public void silenceCellPhones() {
System.out.println("Silencing cell phones");
}
/** * 表演结束,无论表演成功或者失败 */
@After("perform()")
public void finish() {
System.out.println("perform finish");
}
/** * 表演以后,鼓掌 */
@AfterReturning("perform()")
public void applause() {
System.out.println("CLAP CLAP CLAP!!!");
}
/** * 表演失败以后,观众要求退款 */
@AfterThrowing("perform()")
public void demandRefund() {
System.out.println("Demanding a refund");
}
复制代码
新建配置类ConcertConfig以下所示:
package chapter04.concert;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
@Bean
public Audience audience() {
return new Audience();
}
}
复制代码
注意事项:和以往不一样的是,咱们使用了
@EnableAspectJAutoProxy
注解,该注解用来启用自动代理功能。
新建Main类,在其main()方法中添加以下测试代码:
package chapter04.concert;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConcertConfig.class);
Performance performance = context.getBean(Performance.class);
performance.perform();
context.close();
}
}
复制代码
运行代码,输出结果以下所示:
Silencing cell phones
Taking seats
戏剧《不眠之夜Sleep No More》
perform finish
CLAP CLAP CLAP!!!
稍微修改下SleepNoMore类的perform()方法,让它抛出一个异常:
@Override
public void perform() {
int number = 3 / 0;
System.out.println("戏剧《不眠之夜Sleep No More》");
}
复制代码
再次运行代码,输出结果以下所示:
Silencing cell phones
Taking seats
perform finish
Demanding a refund
Exception in thread "main" java.lang.ArithmeticException: / by zero
由此也能够说明,无论目标方法是否执行成功,@After注解都会执行,但@AfterReturning注解只会在目标方法执行成功时执行。
值得注意的是,使用@Aspect
注解的切面类必须是一个bean(无论以何种方式声明),不然切面不会生效,由于AspectJ自动代理只会为使用@Aspect
注解的bean建立代理类。
也就是说,若是咱们将ConcertConfig配置类中的如下代码删除或者注释掉:
@Bean
public Audience audience() {
return new Audience();
}
复制代码
运行结果将变为:
戏剧《不眠之夜Sleep No More》
咱们可使用@Around
注解建立环绕通知,该注解可以让你在调用目标方法先后,自定义本身的逻辑。
所以,咱们以前定义的5个切点,如今能够定义在一个切点中,为不影响以前的切面,咱们新建切面AroundAudience,以下所示:
package chapter04.concert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class AroundAudience {
/** * 可重用的切点 */
@Pointcut("execution(* chapter04.concert.Performance.perform(..))")
public void perform() {
}
@Around("perform()")
public void watchPerform(ProceedingJoinPoint joinPoint) {
try {
System.out.println("Taking seats");
System.out.println("Silencing cell phones");
joinPoint.proceed();
System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable throwable) {
System.out.println("Demanding a refund");
} finally {
System.out.println("perform finish");
}
}
}
复制代码
这里要注意的是,该方法有个ProceedingJoinPoint类型的参数,在方法中能够经过调用它的proceed()方法来调用目标方法。
而后修改下ConcertConfig类的代码:
package chapter04.concert;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
/*@Bean public Audience audience() { return new Audience(); }*/
@Bean
public AroundAudience aroundAudience() {
return new AroundAudience();
}
}
复制代码
运行结果以下所示:
Taking seats
Silencing cell phones
戏剧《不眠之夜Sleep No More》
CLAP CLAP CLAP!!!
perform finish
源码地址:github.com/zwwhnly/spr…,欢迎下载。
Craig Walls 《Spring实战(第4版)》
最后,欢迎关注个人微信公众号:「申城异乡人」,全部博客会同步更新。