一、概述
AOP,面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
二、AOP相关术语
- Aspect(切面):横切关注点的抽象即为切面,它与类相似,只是两者的关注点不一样,类是对物体特征的抽象,而切面是横切关注点的抽象。说白了,切面就是一个类,而横切关注点就是抽取出来的并用于代理其他对象时执行的的方法,比如写日志的方法等待。
- Joinpoint(连接点):连接点就是那些被拦截的点。在spring中只支持方法类型的连接点,说白的就是那些需要被代理的方法。比如说这个方法需要通过面向切面编程去添加日志,那这个方法就是一个连接点。
- Pointcut(切入点):指我们要对那些joinpoint进行拦截的定义。格式:execution(“返回类型”方法所在的类的方法名(参数))。比如execution(* com.MyTest.test(..) ),指的是拦截com包下的MyTest类的test方法,test方法可以是任意返回类型和任意参数。而且这个test也是上面所说的连接点。
- Advice(通知):指的是拦截到joinpoint之后要做的事情,通知分为前置通知、后置通知、异常通知、最终通知和环绕通知。
- Target(目标对象):代理的目标对象,即需要代理的类。
- Weave(织入):指将aspects应用到target对象并导致proxy对象创建的过程就是织入
- Introduction(引入):不修改类代码的前提下,introduction可以在运行期为类动态地添加一些方法或field
三、Spring对AOP的支持
1、 spring提供了4种AOP支持
- 基于代理的经典AOP
- @AspectJ注解驱动的切面
- 纯POJP切面
- 注入式AspectJ切面
2、Spring AOP框架的特点
- Spring创建的通知使用标准的java类编写的
- Spring在运行期间通知对象
- Spring只支持方法连接点
四、使用切面选择连接点(AspectJ切点表达式)
1、Spring AOP所支持的AspectJ切点指示器
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@arg() | 限制连接点匹配参数由指定注解标准的执行方式 |
exceution() | 用于匹配连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
targer() | 限制连接点匹配目标对象所指定类型的类 |
@targer() | 限制连接点匹配特点的执行对象,对象对应的类要具备指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解标准的类型 |
@annotation | 限制匹配带有指定注解连接点 |
注:除了上面的指示器,使用AspectJ其他指示器时,系统会报错
2、切点的编写
- 切点格式:exceution(“返回类型” 方法所在的类型方法名(参数))
- 例:拦截com包下的任意类的test方法,该方法可以是任意返回类型和任意参数
exceution(* com.*.test(..))
- 可以用&&、||等操作符,把execution()与其他指示器连接到一起使用
3、spring的bean()指示器
- bean()中是spring.xml中bean的ID,它与execution()一起使用,表示只有在调用该bean中的该方法才进行拦截
五、基于XML实现AOP
1、spring的AOP配置元素
AOP配置元素 | 描述 |
<aop:advisor> | 定义AOP通知器 |
<aop:before> | 定义前置通知 |
<aop:after-returning> | 定义后置通知 |
<aop:after-throwing> | 定义AOP异常通知 |
<aop:after> | 定义最终通知(不管被通知的方法是否执行成功) |
<aop:around> | 定义AOP环绕通知 |
<aop:aspect> | 定义切面 |
<aop:aspectj-autoproxy> | 启用@AspectJ注解驱动的切面 |
<aop:config> | 顶层AOP配置元素,大多数<aop:*>元素必须包含在<aop:config>元素内 |
<aop:declare-parents> | 为被通知的对象引入额外的接口,并透明的实现 |
<aop:pointcut> | 定义切点 |
2、例子:歌手们表演唱歌,观众作为切面,每当歌手表演前进场inter(),表演成功鼓掌cheer(),表演失败时起哄anger(),最终离场leave()。
- 歌手接口
public interface Performer {
public void perform();
}
- 歌手实现类(目标对象)
public class Singer implements Performer {
private String singerName; //歌手名称
private int age; //歌手年纪
private String songName; //歌曲名称
public void perform(){
System.out.println("我是歌手"+singerName+",今年"+age+"岁,即将演唱的歌曲‘"+ songName+"’");
}
public String getSingerName() {
return singerName;
}
public void setSingerName(String singerName) {
this.singerName = singerName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSongName() {
return songName;
}
public void setSongName(String songName) {
this.songName = songName;
}
}
- 观众类Audience(切面,类的方法就是所谓的连接点)
public class Audience {
public void inter(){
System.out.println("观众进场");
}
public void cheer(){
System.out.println("观众鼓掌");
}
public void anger(){
System.out.println("观众起哄");
}
public void leave(){
System.out.println("观众离场");
}
}
- 添加配置文件
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 歌手 -->
<bean id="singer" class="com.chensr.test.Singer">
<property name="singerName" value="chensr"></property>
<property name="age" value="18"></property>
<property name="songName" value="英雄泪"></property>
</bean>
<!-- 观众 -->
<bean id="audience" class="com.chensr.test.Audience"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 定义切面 -->
<aop:aspect ref="audience">
<!-- 切入点 -->
<aop:pointcut id="performPointcut" expression="execution(* com.chensr.test.Singer.perform(..))"/>
<!-- 前置通知 -->
<aop:before method="inter" pointcut-ref="performPointcut"/>
<!-- 后置通知 -->
<aop:after-returning method="cheer" pointcut-ref="performPointcut"/>
<!-- 异常通知 -->
<aop:after-throwing method="anger" pointcut-ref="performPointcut"/>
<!-- 最后通知通知 -->
<aop:after method="leave" pointcut-ref="performPointcut"/>
</aop:aspect>
</aop:config>
</beans>
注:这里需要引入aspectj相关的jar包,如果是maven,需要如下配置
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
- 编写测试类
public class Test {
@org.junit.Test
public void test(){
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-context.xml");
Performer singer = (Performer)ac.getBean("singer");
singer.perform();
}
}
注:这里获取singer类需要用它的接口Performer接收,因为spring使用的是jdk代理
运行结果:
观众进场
我是歌手chensr,今年18岁,即将演唱的歌曲‘英雄泪’
观众鼓掌
观众离场
- 上面例子中缺少异常通知,异常通知只需修改Singer类的perform方法
public void perform(){
System.out.println("演唱走音,失败了");
throw new RuntimeException("演唱失败");
}
运行结果:
观众进场
演唱走音,失败了
观众起哄
观众离场
3、环绕通知的实现(一个方法包括了上面的前置、后置、异常、最终的通知)
- 修改Audience类
public class Audience {
public void watchPerform(ProceedingJoinPoint joinPoint){
try{
System.out.println("观众进场");
joinPoint.proceed();
System.out.println("观众鼓掌");
}catch (Throwable e){
System.out.println("观众起哄");
}finally {
System.out.println("观众离场");
}
}
}
- 修改AOP配置
<aop:config>
<!-- 定义切面 -->
<aop:aspect ref="audience">
<!-- 切入点 -->
<aop:pointcut id="performPointcut" expression="execution(* com.chensr.test.Singer.perform(..))"/>
<!-- 环绕通知 -->
<aop:around method="watchPerform" pointcut-ref="performPointcut"/>
</aop:aspect>
</aop:config>
运行结果和上面的例子一致
注:ProceedingJoinPoint.proceed()表示在通知里调用被通知的方法。以前用环绕通知实现“应用里一条sql执行的总时间”。
4、参数的传递(歌手换另外歌手的一首歌,观众高呼这首歌的名称)
- 为Performer接口添加changSong方法
public interface Performer {
public void perform();
public void changeSong(String otherSinger,String songName);
}
- 修改歌手类实现,添加changeSong(String otherSinger,String songName)
public class Singer implements Performer {
private String singerName; //歌手名称
private int age; //歌手年纪
private String songName; //歌曲名称
public void perform(){
System.out.println("我是歌手"+singerName+",今年"+age+"岁,即将演唱的歌曲‘"+ songName+"’");
}
public void changeSong(String otherSinger,String songName){
System.out.println("大家好,我将演唱"+otherSinger+"的“"+songName+"”");
}
public String getSingerName() {
return singerName;
}
public void setSingerName(String singerName) {
this.singerName = singerName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSongName() {
return songName;
}
public void setSongName(String songName) {
this.songName = songName;
}
}
- 为Audience添加欢呼方法yell(String otherSinger,String songName)
public class Audience {
public void yell(String otherSinger,String songName){
System.out.println("观众:我喜欢听你唱"+otherSinger+"的“"+songName+"”");
}
}
- 修改AOP的配置
<!-- aop配置 -->
<aop:config>
<!-- 定义切面 -->
<aop:aspect ref="audience">
<aop:pointcut id="performPointcut" expression="execution(* com.chensr.test.Singer.changeSong(String,String)) and args(otherSinger,songName)"/>
<aop:after-returning method="yell" pointcut-ref="performPointcut" arg-names="otherSinger,songName"/>
</aop:aspect>
</aop:config>
注:切点定义了拦截changeSong方法,该方法有两个String参数,并且有参数名分别为args(otherSinger,songName);后置通知定义了该通知接收了两个参数。Xml定义配置的参数名必须跟代码的方法名一样
- 修改测试类
public class Test {
@org.junit.Test
public void test(){
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-context.xml");
Performer singer = (Performer)ac.getBean("singer");
singer.changeSong("chensr","葫芦娃");
}
}
运行结果:
大家好,我将演唱chensr的“葫芦娃”
观众:我喜欢听你唱chensr的“葫芦娃”
注:对于环绕通知aop:around的传参,不需要在切点中配置args(otherSinger,songName),也不需要在通知里添加arg-names="otherSinger,songName"。只需要用ProceedingJoinPoint.getArgs()就能获取被代理的方法的参数
六、基于注解实现AOP
- 歌手类Singer有perform、changeSong两个方法
public class Singer implements Performer {
private String singerName; //歌手名称
private int age; //歌手年纪
private String songName; //歌曲名称
public void perform(){
System.out.println("我是歌手"+singerName+",今年"+age+"岁,即将演唱的歌曲‘"+ songName+"’");
}
public void changeSong(String otherSinger,String songName){
System.out.println("大家好,我将演唱"+otherSinger+"的“"+songName+"”");
}
public String getSingerName() {
return singerName;
}
public void setSingerName(String singerName) {
this.singerName = singerName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSongName() {
return songName;
}
public void setSongName(String songName) {
this.songName = songName;
}
}
- Audience类,定义两个切点perform、yell
@Aspect
public class Audience {
//表演切点
@Pointcut("execution(* com.chensr.test.Singer.perform(..))")
public void perform(){};
//换歌切点
@Pointcut("execution(* com.chensr.test.Singer.changeSong(..))")
public void changeSongName(){};
@AfterReturning("changeSongName() and args(otherSinger,songName)")
public void yell(String otherSinger,String songName){
System.out.println("观众:我喜欢听你唱'"+otherSinger+"的“"+songName+"”");
}
@Before("perform()")
public void inter(){
System.out.println("观众进场");
}
@AfterReturning("perform()")
public void cheer(){
System.out.println("观众鼓掌");
}
@AfterThrowing("perform()")
public void anger(){
System.out.println("观众起哄");
}
@After("perform()")
public void leave(){
System.out.println("观众离场");
}
}
注:@Aspect表示切面,@Pointcut表示切点(切点下的方法名表示为切点定义名称),@Before表示前置通知,@AfterReturning表示后置,@AfterThrowing表示异常通知,@After表示最终通知
- 修改配置文件,开启AOP的注解<aop:aspectj-autoproxy/>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 歌手 -->
<bean id="singer" class="com.chensr.test.Singer">
<property name="singerName" value="chensr"></property>
<property name="age" value="18"></property>
<property name="songName" value="英雄泪"></property>
</bean>
<!-- 观众 -->
<bean id="audience" class="com.chensr.test.Audience"></bean>
<!-- 基于注解的aop配置 -->
<aop:aspectj-autoproxy/>
</beans>
- 修改测试类
public class Test {
@org.junit.Test
public void test(){
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-context.xml");
Performer singer = (Performer)ac.getBean("singer");
singer.perform();
System.out.println("*************************************");
singer.changeSong("chensr","葫芦娃");
}
}
运行结果:
观众进场
我是歌手chensr,今年18岁,即将演唱的歌曲‘英雄泪’
观众离场
观众鼓掌
*************************************
大家好,我将演唱chensr的“葫芦娃”
观众:我喜欢听你唱'chensr的“葫芦娃”
- 环绕通知只需要加上@Around("perform()")
@Around("perform()")
public void watchPerform(ProceedingJoinPoint joinPoint){
try{
System.out.println("观众进场");
joinPoint.proceed();
System.out.println("观众鼓掌");
}catch (Throwable e){
System.out.println("观众起哄");
}finally {
System.out.println("观众离场");
}
}