Spring AOP
AOP(Aspect Oriented Programming),即面向切面编程,官方的解释是:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。换一个相对好理解的说法,就是可以把程序中重复的代码(日志记录、事务管理等)抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上去执行,实现对目标对象方法、参数的拦截,可在目标对象前后等位置添加上抽取出来的功能,实现其功能上的增强。以减少重复代码,提高开发效率,方便维护。
AOP 也是 Spring 框架中的主要内容,但 Spring AOP 只支持目标对象方法的拦截、增强,Spring AOP 常用的实现方式有基于XML和基于注解两种。在学习这两种方式前,我们有必要先了解 AOP 中的一些概念。
一、AOP 的相关概念
- 连接点(JoinPoint):在Spring中代表类中的定义的方法
- 切入点(Pointcut):指定要对哪些连接点(方法)进行拦截、增强,需要通过切入点表达式来指定哪些方法可以作为切入点。
- 通知(Advice):拦截到切入点后要做的事情就是通知,即要增强什么功能
- 目标对象(Target):要被代理的目标对象
- 织入(Weaving):把通知应用到目标对象,来创建增强的代理对象的过程,若目标对象的类实现了接口,Spring 默认采用JDK动态代理实现织入,否则采用CGLIB动态代理
- 代理对象(Proxy):通过织入产生的代理对象
- 切面(Aspect):切入点和通知的结合称作切面
上边提到的通知按照用途分为以下几种:
- 前置通知:在切入点方法之前执行
- 后置通知:在切入点方法正常还执行完后执行,和异常通知只会执行其中一个
- 异常通知:当切入点方法发生异常时执行,和后置通知只会执行其中一个
- 最终通知:无论切入点方法是否发生异常都会执行
-
环绕通知:环绕通知更加灵活,可以不用配置上边四种通知来实现切入点方法的增强,让开发者通过编码的方式主动控制增强代码执行的时机。但这要求开发者必须主动调用切入点方法。Spring 提供了
ProceedingJoinPoint
接口,该接口可以作为环绕通知的方法参数,调用它的proceed()
方法,就相当于调用切入点方法。
注意:环绕通知和前边四种通知不要一起使用。
还有一个知识点需要我们先了解下,那就是切入点表达式,先看一个切入点表达式:
execution(* com.shh.aop.CoffeeShop.sale(..))
它的作用就是匹配com.shh.aop
包下,CoffeeShop
类的sale()
方法,实现拦截。先分析从这个切入点表达式可以看到的一些信息:
- execution:使用该关键字定义切入点表达式
- *:星号代表通配符,可以匹配返回值、包名、类名、方法
- (..):方法名后边括号中的
..
表示方法的任意参数(包括无参),也可以显式的指定参数类型,例如int、java.lang.String
等 - 切入点表达式中可以省略方法的权限修饰符
切入点表达式的定义很灵活,可以根据实际的需求变通,例如:
execution(* com.shh.aop.CoffeeShop.*(..))
表示会拦截com.shh.aop
包下,CoffeeShop
类的所有方法。
execution(* com.shh.aop.*.*(..))
表示会拦截com.shh.aop
包下所有类的所有方法。
有了这些基础知识的铺垫,就更好理解后边的内容了。Spring AOP 使用的例子,会在之前 Java 动态代理 中例子的基础上扩展。
二、基于xml的AOP使用
示例代码要实现的功能大致是:有一个CoffeeShop
实现类,其中sale()
仅负责售卖咖啡,但我们希望增强sale()
方法的功能,在sale()
执行前先向客户问好,如果sale()
正常执行结束则去提示用户付款,否则提示错误信息,最后向用户告别。
首先定义Shop
接口:
public interface Shop {
void sale(String name);
}
CoffeeShop
类实现Shop
接口,重写了要被拦截、增强的sale()
方法,如果sale()
方法接收到的参数为空,则直接抛出异常:
public class CoffeeShop implements Shop {
public void sale(String name) {
if (!StringUtils.isEmpty(name)) {
System.out.println("开始制作" + name + "......制作完成!");
} else {
throw new RuntimeException();
}
}
}
这样被代理的类就定义好了,先放着后边再用。然后定义通知类,也就是要对CoffeeShop
类的sale()
方法增强哪些功能:
public class Greet {
public void welcome() {
System.out.println("前置通知:欢迎!");
}
public void cashier() {
System.out.println("后置通知:请扫码付款!");
}
public void soldOut() {
System.out.println("异常通知:商品名不能为空!");
}
public void goodbye() {
System.out.println("最终通知:再见!");
}
}
然后通过xml来配置AOP:
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--将CoffeeShop类交给IoC管理-->
<bean id="coffeeShop" class="com.shh.aop.CoffeeShop"/>
<!--将招呼的通知类交给IoC管理-->
<bean id="greet" class="com.shh.aop.Greet"/>
<!--Spring中基于xml的AOP配置-->
<!--1.使用<aop:pointcut>定义切入点表达式,注意要定义在<aop:aspect>前,如果定义在<aop:aspect>标签里只能当前切面使用,不能公用-->
<!--2.使用<aop:config>标签开始配置AOP-->
<!--3.使用<aop:aspect>标签配置切面
id:切面的唯一标识。
ref:需要引用通知类的bean id。-->
<!--4.使用<aop:before>标签配置前置通知,欢迎客户
method:用Greet类的那个方法作为通知方法。
pointcut-ref:配置切入点表达式的引用,指定要对CoffeeShop类中的那些方法使用前置通知,实现增强。
使用<aop:after-returning>标签配置后置通知,提醒客户支付
使用<aop:after-throwing>标签配置异常通知,商品信息有误时的处理
使用<aop:after>标签配置最终通知,和客户告别-->
<aop:config>
<aop:pointcut id="sale" expression="execution(* com.shh.aop.CoffeeShop.sale(..))"/>
<aop:aspect id="greetAdvice" ref="greet">
<aop:before method="welcome" pointcut-ref="sale"/>
<aop:after-returning method="cashier" pointcut-ref="sale"/>
<aop:after-throwing method="soldOut" pointcut-ref="sale"/>
<aop:after method="goodbye" pointcut-ref="sale"/>
</aop:aspect>
</aop:config>
</beans>
关键的说明信息都在注释里边了,到这里编码配置工作就结束了,接下来就是测试:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:aop.xml"})
public class AOPTest {
@Autowired
Shop coffeeShop;
@Test
public void saleTest() {
coffeeShop.sale("拿铁");
}
}
输出:
如果商品名为空:
coffeeShop.sale("")
,则会有异常,是这样的输出结果:这也符合我们开始设定的场景,经过xml中的通知配置,额外的功能按约定好的规则自动添加到了要被增强的方法前后。可以看出后置通知和异常通知只会执行其中一个。
接下来使用环绕通知实现这个功能,首先修改通知类,只有一个greeting()
方法:
public class Greet {
/**
* 环绕通知
*/
public Object greeting(ProceedingJoinPoint pjp) {
try {
System.out.println("前置通知:欢迎!");
// 获取切入点方法的参数
Object[] params = pjp.getArgs();
// 主动调用切入点方法(即执行sale()方法)
Object result = pjp.proceed(params);
System.out.println("后置通知:请扫码付款!");
return result;
} catch (Throwable throwable) {
System.out.println("异常通知:商品名不能为空!");
throw new RuntimeException(throwable);
} finally {
System.out.println("最终通知:再见!");
}
}
}
注意切入点方法的调用,即要增强的方法需要我们主动调用,然后需要我们在合适位置自行添加要增强的功能即可。
再修改xml配置文件,配置环绕通知:
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--将CoffeeShop类交给IoC管理-->
<bean id="coffeeShop" class="com.shh.aop.CoffeeShop"/>
<!--将招呼的通知类交给IoC管理-->
<bean id="greet" class="com.shh.aop.Greet"/>
<!--Spring中基于xml的AOP配置-->
<aop:config>
<aop:pointcut id="sale" expression="execution(* com.shh.aop.CoffeeShop.sale(..))"/>
<aop:aspect id="greetAdvice" ref="greet">
<aop:around method="greeting" pointcut-ref="sale"/>
</aop:aspect>
</aop:config>
</beans>
同样可以实现上边的效果。
三、基于注解的AOP使用
使用注解配置时,就是要用对应的注解,替换掉之前xml中的配置:
<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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="coffeeShop" class="com.shh.aop.CoffeeShop"/>
<bean id="greet" class="com.shh.aop.Greet"/>
<aop:config>
<aop:pointcut id="sale" expression="execution(* com.shh.aop.CoffeeShop.sale(..))"/>
<aop:aspect id="greetAdvice" ref="greet">
<aop:before method="welcome" pointcut-ref="sale"/>
<aop:after-returning method="cashier" pointcut-ref="sale"/>
<aop:after-throwing method="soldOut" pointcut-ref="sale"/>
<aop:after method="goodbye" pointcut-ref="sale"/>
</aop:aspect>
</aop:config>
</beans>
-
<bean>
可以用@Component
注解代替 -
<aop:aspect>
可以用@Aspect
注解代替,来配置切面类 -
<aop:pointcut>
可以用@Pointcut
注解代替,来配置切点 -
<aop:before>
、<aop:after-returning>
、<aop:after-throwing>
、<aop:after>
、<aop:around>
分别对应@Before
、@AfterReturning
、@AfterThrowing
、@After
、@Around
注解,来配置各种通知
修改CoffeeShop
类,添加@Component
:
@Component
public class CoffeeShop implements Shop {
......
}
新建Greet2
类,使用@Aspect
、@Component
,即切面类:
@Aspect
public class Greet2 {
/**
* 定义切入点
*/
@Pointcut("execution(* com.shh.aop.CoffeeShop.sale(..))")
public void sale() {
}
/**
* 注解的参数为切入点的引用
*/
@Before("sale()")
public void welcome() {
System.out.println("前置通知:欢迎!");
}
@AfterReturning("sale()")
public void cashier() {
System.out.println("后置通知:请扫码付款!");
}
@AfterThrowing("sale()")
public void soldOut() {
System.out.println("异常通知:商品名不能为空!");
}
@After("sale()")
public void goodbye() {
System.out.println("最终通知:再见!");
}
}
到这里基于注解的AOP配置就基本完了,用一个切面类替换掉了之前xml中配置。
最后就是开启 Spring 对基于注解AOP的支持,以及创建IoC容器时要扫描的包,有两种方式可选:xml配置、java配置类。
xml配置方式如下:
<?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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!--配置创建IoC容器时要扫描的包-->
<context:component-scan base-package="com.shh.aop"/>
<!--开启Spring对基于注解AOP的支持-->
<aop:aspectj-autoproxy/>
</beans>
java配置类的方式如下:
@Configuration
@ComponentScan(basePackages = "com.shh.aop")
@EnableAspectJAutoProxy
public class AOPConfig {
}
根据自己的实际需求选择即可。
接下来测试一下:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AOPConfig.class})
public class AOPTest {
@Autowired
Shop coffeeShop;
@Test
public void saleTest() {
coffeeShop.sale("拿铁");
}
}
如果商品名为空:
coffeeShop.sale("")
,则会有异常,是这样的输出结果:注意,从图中可以看出,后置通知、异常通知始终是最后输出的,按照正常的逻辑应该是最终通知最后输出,而使用基于xml的AOP配置时确实正常的期望结果,这一点需要注意!!!
但是如果使用基于注解的环绕通知则不会用这样的问题,毕竟环绕通知更加灵活,切入点方法和增强内容的执行顺序可以由我们控制:
只需修改切面类,定义环绕通知的配置方法:
@Component
@Aspect
public class Greet2 {
/**
* 定义切入点
*/
@Pointcut("execution(* com.shh.aop.CoffeeShop.sale(..))")
public void sale() {
}
/**
* 环绕通知
*/
@Around("sale()")
public Object greeting(ProceedingJoinPoint pjp) {
try {
System.out.println("前置通知:欢迎!");
// 获取切入点方法的参数
Object[] params = pjp.getArgs();
// 主动调用切入点方法
Object result = pjp.proceed(params);
System.out.println("后置通知:请扫码付款!");
return result;
} catch (Throwable throwable) {
System.out.println("异常通知:商品名不能为空!");
throw new RuntimeException(throwable);
} finally {
System.out.println("最终通知:再见!");
}
}
}
测试结果如下:
关于 Spring AOP 的内容就先到这里了。