一、Spring面向切面编程的基本概念
面向切面编程(即AOP):把项目中需要再多处使用的功能比如日志、安全和事务等集中到一个类中处理,而不用在每个需要用到该功能的地方显式调用。
横切关注点:在软件开发过程中分散于应用中多处的功能
切面:切点和通知的结合,通知和切点共同定义了切面的全部内容:是什么以及在何时和何处完成其功能
通知:切面要完成的工作。除了描述切面要完成的工作,还解决了何时完成何时执行这个工作的问题。Spring的通知有5种类型:before、after、after-returnning、after-throwing和around这五种类型
连接点:连接点表示在何种操作发生时应用切面。比如方法调用时、修改字段时以及抛出异常时;
切点:一般使用明确的类和方法名称或是利用正则表达式匹配的类和方法名称来指定在何处应用切面,一般应用切面的点就被称为切点,一般使用切点来指定连接点。
引入:我们可以创建一个通知类创建新的属性和方法,就可以在不修改切点类的前提下让他们具有新的行为功能
织入:织入是指把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象,在目标对象的生命周期里有多个点可以进行织入:
编译期:切面在目标类编译时被织入(例如AspectJ的织入编译器织入切面)
类加载期:切面在目标类加载到JVM时被织入(例如AspectJ5的加载时织入)
运行期:切面在运行时的某个时刻被织入(Spring AOP以这种方式织入切面)
二、Spring对AOP的支持
Spring提供了五种类型的AOP支持:
1)基于代理的经典Spring AOP;
2)纯POJO切面;
3)@AspectJ注解驱动的切面;
4)注入式AspectJ切面
前三种方式均为Spring AOP实现的变体,构建在动态代理的基础上,因此Spring对AOP的支持局限于方法拦截
1 - 编写Spring切点
Spring支持的AspectJ切点指示器
连接点=切点+执行时机
AspectJ指示器 | 描述 |
args() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配连接点的执行方法,通常用于编写切点表达式的指示器 |
this() | 限制连接点匹配AOP代理的Bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解标注的类型(当使用Spring AOP时,切点方法定义在由指定注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
切点表达式的组成详解
execution(* concert.Performance.perform(..))
下面解析下表达式各个部分
* : 表明该切点表达式不关心方法返回值类型,任何返回值类型均匹配
concert.Performance : 这部分指定方法所属类,用于限定切点匹配的类,可以使用*模糊匹配
perform : 切点匹配的方法名
(..) : 两个点号表明切点匹配任意perform的重载方法无论入参是什么。
在切点表达式中限制选择范围
除了上述Spring支持的AspectJ切点指示器之外,Spring引入了新的bean()指示器,他支持基于Bean Id或Bean Name作为参数限制切点只匹配特定Bean。我们可以在 xml或者注解配置中基于逻辑操作符and、or组合使用execution和其他指示器来进一步限制匹配切点,例如:
execution(* concert.Performance.perform()) and bean('woodstock')
三、使用注解创建切面
Spring提供的定义通知的五个注解
注解 | 通知 |
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用之前执行 |
1 - 定义切面
目标类
@Component
public class Performance {
public void perform(){
System.out.println("method perform invoking");
}
}
定义切面代码
@Component
@Aspect
public class PerformanceAspect {
@Pointcut("execution (* com.example.demo.service.Performance.perform(..))")
public void performancePointCut(){
}
@Before("performancePointCut()")
public void beforePerform(){
System.out.println("before method invoke");
}
@Around("performancePointCut()")
public void handler(ProceedingJoinPoint point) {
try {
System.out.println("method perform invoke Around start");
point.proceed();
System.out.println("method perform invoke Around end");
}catch(Throwable ex){
System.out.println("method perform throw an exception ");
}
}
}
测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public class PerformanceAspectTest {
@Autowired
private Performance performance;
@Test
public void testAspect(){
performance.perform();
}
}
2 - 在处理通知中的参数
如果切面所通知的方法含有参数切点表达式怎么将参数传递给通知方法,相关代码如下:
目标类代码
@Component
public class Performance {
public void perform(String userName){
System.out.println(userName+" is performing");
}
}
切面类代码
@Component
@Aspect
public class PerformanceAspect {
@Pointcut("execution (* com.example.demo.service.Performance.perform(String))" +"&& args(userName)")
public void performancePointCut(String userName){
}
@Before("performancePointCut(userName)")
public void beforePerform(String userName){
System.out.println(userName+" before perform");
}
@Around("performancePointCut(userName)")
public void handler(ProceedingJoinPoint point, String userName) {
try {
System.out.println(userName+" perform Around start");
point.proceed();
System.out.println(userName+" perform Around end");
}catch(Throwable ex){
System.out.println(userName+" perform throw an exception ");
}
}
}
测试类代码
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public class PerformanceAspectTest {
@Autowired
private Performance performance;
@Test
public void testAspect(){
performance.perform("zhangsan");
}
}
3 - 基于注解为Spring Bean引入新功能
利用引入的AOP概念,切面可以为Spring Bean添加新的方法。其基本实现原理是基于自动动态代理机制,用户通过实现一个新的接口并把该接口引入Bean中,这样看起来Bean似乎实现了该接口的方法但实际上是Bean对应的代理类接口实现被拆分到了多个类中,每次Spring发现一个Bean实现了@Aspect注解时都会为他创建一个代理,然后方法调用都会委托给被代理的bean或者被引入的接口实现,这取决于调用方法属于被代理Bean还是引入接口。以下是使用示例:
public interface PerformanceEncorable {
void performanceEncore();
}
@Component
@Aspect
public class PerformanceEncorableIntroducer {
@DeclareParents(value="com.example.demo.service.Performance", defaultImpl = DefaultPerformanceEncorableImpl.class)
@Autowired
private PerformanceEncorable performanceEncorable;
}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DemoApplication.class)
public class PerformanceAspectTest {
@Autowired
private Performance performance;
@Test
public void testAspect(){
performance.perform("zhangsan");
PerformanceEncorable performanceEncorable = (PerformanceEncorable)performance;
performanceEncorable.performanceEncore();
}
}
四、使用XML创建切面
Spring的AOP命名空间提供的声明切面相关的元素
AOP配置元素 | 用途描述 |
<aop:advisor> | 定义aop通知器 |
<aop:after> | 定义aop后置通知 |
<aop:after-returning> | 定义aop返回通知 |
<aop:after-throwing> | 定义aop的异常通知 |
<aop:around> | 定义aop环绕通知 |
<aop:aspect> | 定义一个切面 |
<aop:asoectj-autoproxy> | 启用@AspectJ注解驱动的切面 |
<aop:before> | 定义一个AOP前置通知 |
<aop:config> | 顶层的AOP配置元素 |
<aop:delare-parents> | 以透明的方式为被通知的对象引入额外的接口 |
<aop:pointcut> | 定义一个切点 |
直接基于上述例子列举XML方式的实现
1 - 在XML中声明切面
<aop:config>
<aop:aspect ref="performanceAspect">
<aop:pointcut id="performancePointCut" expression="execution(* com.example.demo.service.Performance.perform(..))" />
<aop:before pointcut-ref="performancePointCut" method="beforePerform" />
<aop:around pointcut-ref="performancePointCut" method="handler" />
</aop:aspect>
</aop:config>
目标类和切面类
@Component
public class Performance {
public void perform(String userName){
System.out.println(userName+" is performing");
}
}
@Component
public class PerformanceAspect {
public void beforePerform(String userName){
System.out.println(userName+" before perform");
}
public void handler(ProceedingJoinPoint point, String userName) {
try {
System.out.println(userName+" perform Around start");
point.proceed();
System.out.println(userName+" perform Around end");
}catch(Throwable ex){
System.out.println(userName+" perform throw an exception ");
}
}
}
2 - 通过切面引入新的功能
引入接口于默认实现类
public interface PerformanceEncorable {
void performanceEncore();
}
public class DefaultPerformanceEncorableImpl implements PerformanceEncorable{
@Override
public void performanceEncore() {
System.out.println("method performanceEncore invoke");
}
}
XML配置如下
<aop:config>
<aop:aspect>
<aop:declare-parents types-matching="com.example.demo.service.Performance+" implement-interface="com.example.demo.service.PerformanceEncorable"
default-impl="com.example.demo.service.DefaultPerformanceEncorableImpl" />
</aop:aspect>
</aop:config>
<aop:declare-parents>声明了此切面所通知的bean要在他们的对象层次结构上有新的父类型,具体到本例子中即为所有实现了Performance接口的Bean示例在他们的父类结构上会增加PerformanceEncorable接口,除了使用上述default-impl方式标识引入接口实现还可以使用delegate-ref属性来标识,如下所示:
<aop:config>
<aop:aspect>
<aop:declare-parents types-matching="com.example.demo.service.Performance+" implement-interface="com.example.demo.service.PerformanceEncorable"
delegate-ref="defaultPerformanceEncorableImpl" />
</aop:aspect>
</aop:config>
五、注入Aspect切面
虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP 是一个功能比较弱的AOP解决方案。AspectJ提供了Spring AOP所不能支持的许多类型的切点。例如,当我们需要在创建对象时应用通知,构造器切点就非常方便。Spring基于代理的AOP无法把通知应用于对象的创建过程。
1 - 定义切面
public aspect ConstructAspect {
public ConstructAspect(){}
/**
* 定义切点
*/
pointcut performance():execution(* com.example.demo.service.Performance.perform(..));
pointcut construct():execution(aop.kind4.CriticismEngineImpl.new());
before() : performance(){
System.out.println("before performance method invoke");
}
after():construct(){
System.out.println("Performance construct end");
}
before():construct(){
System.out.println("Performance construct begin");
}
}
XML配置
<bean class="com.example.demo.service.ConstructAspect" factory-method="aspectOf">
</bean>