原文地址:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop
一、基本概念
1、AOP
面向切面编程:Aspect Oriented Programming,对于项目业务所关心的处理逻辑之外的、覆盖多个模块的、相似的功能,我们可以将其收集在一个横切面中来解决这类混杂、分散的功能,这将会使我们更好的专注于业务逻辑。例如:事务管理、日志记录、参数校验、服务调用信息打印、锁重试等等。
2、相关术语
①切面(aspect):模块化的横切关注点组成的对象
②横切关注点(crossingcut concern point):与多个类相关的功能点
③通知(advice):用来完成切面对目标对象要做的任务
④连接点(joinPoint):方法执行的位置,也就是通知要作用的位置
⑤切点(pointCut):匹配和查找需要通知的连接点
⑥目标(targetObject):需要切面执行通知的目标对象
⑦代理对象(proxyObject):为目标对象完成切面功能而创建的代理对象
3、五种通知类型
①前置通知(BeforeAdvice):方法执行前执行的通知
②返回通知(AfterReturningAdvice):方法正常成功返回执行的通知,可以返回结果值
③异常通知(AfterThrowingAdvice):方法发生异常执行的通知,可以返回异常信息
④后置通知(或最终通知)(AfterAdvice):方法最终退出执行的通知,不管正常返回还是发生异常都会执行,类似try-catch中的finally
⑤环绕通知(AroundAdvice):方法执行的周围都会执行的通知,能够完成①②③④合在一起的通知任务
二、以@AspectJ注解形式使用AOP
1、Java配置启用切面自动代理配置
package com.csdn.spring.aop;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}
2、编写切面并由IOC容器管理
package com.csdn.spring.aop;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class aopAspect {
}
3、利用切点表达式设置切点
package com.csdn.spring.aop;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AopAspect {
/**
* 共通切点
* 切点表达式中的execution方法:使用通配符 * 匹配 所有包中修饰符是
* public、任意返回类型、任意参数的方法作为切点
*/
@PointCut("execution(public * *.(..))")
public void commonPonitCut() {}
/**
* 特殊切点
* 限制切点必须是名称为transfer()的方法及重载的方法
*/
@PointCut("within(* transfer(..))")
public void specialPonitCut() {}
/**
* 结合共通切点和特殊切点, 可以使用 && || !
*/
@PointCut("commonPonitCut() && specialPonitCut()")
public void combinePonitCut() {}
}
4、设置通知及切点
package com.csdn.spring.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AopAspect {
/**
* 共通切点
* 切点表达式中的execution方法:使用通配符 * 匹配 所有包中修饰符是
* public、任意返回类型、任意参数的方法作为切点
*/
@PointCut("execution(public * *(..))")
public void commonPointCut() {}
/**
* 特殊切点
* 限制切点必须是名称为包com.csdn.spring.aop下的方法
*/
@PointCut("within(com.csdn.spring.aop..*)")
public void specialPointCut() {}
/**
* 特殊切点
* 限制切点必须是名称为divide的方法
*/
@PointCut("execution(* divide(..))")
public void dividePointCut() {}
/**
* 结合共通切点和特殊切点
* 注意:由于测试需要,所以不匹配divide方法
* 可以使用 && || !
*/
@PointCut("commonPointCut() && specialPointCut() && (!dividePointCut())")
public void combinePointCut() {}
/**
* 前置通知
* JoinPoint:可以获取到连接点处的切面信息,签名(方法声明)、目标对象、代理对象、方法入参、方法描述
*/
@Before("combinePointCut()")
public void beforeAdvice(JoinPoint jp) {
// 签名,即方法声明
Signature signature = jp.getSignature();
// 方法名
String methodName = signature.getName();
// 入参数组
Object[] args = jp.getArgs();
// 拼接入参为字符串用于输出
StringBuffer argStr = new StringBuffer("");
if (args != null) {
for (Object arg : args) {
argStr.append(arg);
argStr.append(" ");
}
}
// 输出信息
System.out.println("beforeAdvice...前置通知执行:" + methodName + ", 入参:" + argStr.toString());
}
/**
* 返回通知
* returning属性指定返回变量名称要与通知声明的参数名称一致
* result:通知声明的返回值类型可以任意
*/
@AfterReturning(pointcut = "combinePointCut()", returning = "result")
public void afterReturningAdvice(JoinPoint jp, Object result) {
// 签名,即方法声明
Signature signature = jp.getSignature();
// 方法名
String methodName = signature.getName();
// 输出信息
System.out.println("afterReturningAdvice...返回通知执行:" + methodName + ", 结果:" + result);
}
/**
* 异常通知
* 注意:由于测试需要,只匹配divide方法
* throwing属性指定异常变量名称要与通知声明的参数名称一致
* e:通知声明的异常类型可以任意
*/
@AfterThrowing(pointcut = "dividePointCut()", throwing= "e")
public void afterThrowingAdvice(JoinPoint jp, Throwable e) {
// 签名,即方法声明
Signature signature = jp.getSignature();
// 方法名
String methodName = signature.getName();
// 输出信息
System.out.println("afterThrowingAdvice...异常通知执行:" + methodName + ", 异常信息:" + e);
}
/**
* 后置通知(或最终通知)
*/
@After("combinePointCut()")
public void afterAdvice(JoinPoint jp) {
// 签名,即方法声明
Signature signature = jp.getSignature();
// 方法名
String methodName = signature.getName();
// 输出信息
System.out.println("afterAdvice...后置通知执行:" + methodName);
}
/**
* 环绕通知
* 注意:由于测试需要,只匹配divide方法
* ProceedingJoinPoint是JoinPoint子类,必须声明此参数
*/
@Around("dividePointCut()")
public Object aroundAdvice(ProceedingJoinPoint pjp) {
// 签名,即方法声明
Signature signature = pjp.getSignature();
// 方法名
String methodName = signature.getName();
// 入参数组
Object[] args = pjp.getArgs();
// 拼接入参为字符串用于输出
StringBuffer argStr = new StringBuffer("");
if (args != null) {
for (Object arg : args) {
argStr.append(arg);
argStr.append(" ");
}
}
// 类似前置通知
System.out.println("aroundAdvice...环绕通知执行:" + methodName + ", 入参:" + argStr.toString());
Object retValue = null;
try {
retValue = pjp.proceed();
// 类似返回通知
System.out.println("aroundAdvice...环绕通知返回:" + methodName + ", 结果:" + retValue);
} catch (Throwable e) {
// 类似异常通知
System.out.println("aroundAdvice...环绕通知异常:" + methodName + ", 异常信息:" + e);
}
// 类似后置通知
System.out.println("aroundAdvice...环绕通知完成:" + methodName);
return retValue;
}
}
5、创建计算类进行测试
①创建spring的applicationContext.xml配置文件,开启组件扫描
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.csdn.spring"/>
</beans>
②在包com.csdn.spring或其子包下创建计算类并添加方法
package com.csdn.spring.aop;
import org.springframework.stereotype.Component;
/**
* 计算类需要被IOC容器管理,因此要加注解@Component使其可以被扫描到
*/
@Component
public class Calculator {
/**
* 两数相加
* 返回包装类型是因为在环绕通知返回的类型为引用类型,使用基本类型会出现拆箱失败错误
*/
public Integer add(int i, int j) {
return i + j;
}
/**
* 两数相除
* 返回类型说明:参考add()方法
*/
public Double divide(int i, int j) {
return Double.valueOf(i/j);
}
}
③编写main方法执行计算验证AOP
import com.csdn.spring.aop.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringApplication {
public static void main(String[] args) {
// 读取类路径下的spring配置文件创建IOC容器
ApplicationContext cxt = new
ClassPathXmlApplicationContext("applicationContext.xml");
// 从IOC容器中根据Beanm名称获取bean实例
Calculator calculator = (Calculator) cxt.getBean("calculator");
// 使用bean实例,调用加法
Integer addResult = calculator.add(1, 1);
// 输出计算结果
System.out.println("calculator.add's result: " + addResult);
// 调用除法
Double divideResult = calculator.divide(4, 2);
// 输出计算结果
System.out.println("calculator.divide's result: " + divideResult);
}
}
控制台输出:根据切点设置,加法没有执行环绕通知,除法只执行了环绕通知
beforeAdvice...前置通知执行:add, 入参:1 1
afterReturningAdvice...返回通知执行:add, 结果:2
afterAdvice...后置通知执行:add
calculator.add's result: 2
aroundAdvice...环绕通知执行:divide, 入参:
aroundAdvice...环绕通知返回:divide, 结果:2.0
aroundAdvice...环绕通知完成:divide
calculator.divide's result: 2.0
前面的测试并没有执行异常通知,下面测试
import com.csdn.spring.aop.Calculator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringApplication {
public static void main(String[] args) {
// 读取类路径下的spring配置文件创建IOC容器
ApplicationContext cxt = new
ClassPathXmlApplicationContext("applicationContext.xml");
// 从IOC容器中根据Beanm名称获取bean实例
Calculator calculator = (Calculator) cxt.getBean("calculator");
// 调用除法,除数是0发生异常
Double divideResult = calculator.divide(4, 0);
// 输出计算结果
System.out.println("calculator.divide's result: " + divideResult);
}
}
控制台输出:根据切点设置,环绕通知和异常通知都匹配divide方法的切点,所以发生异常时都执行了并打印异常。注意,返回通知没有执行是因为切点设置时将divide方法排除匹配范围了,否则是会执行的。
aroundAdvice...环绕通知执行:divide, 入参:4 0
afterThrowingAdvice...异常通知执行:divide, 异常信息:java.lang.ArithmeticException: / by zero
aroundAdvice...环绕通知异常:divide, 异常信息:java.lang.ArithmeticException: / by zero
aroundAdvice...环绕通知完成:divide
calculator.divide's result: null
三、使用注解@Pointcut的指示符定义切点表达式
注:连接点是指使用AOP时方法的执行
①execution
: 匹配方法执行连接点,AOP中主要使用的指示符。
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
modifiers-pattern:修饰符模式,可选
ret-type-pattern:返回类型模式,必填,返回类型模式确定方法的返回类型必须是什么,才能匹配连接点,* 可以匹配任意返回类型。
declaring-type-pattern:声明类型模式,可选
name-pattern:名称模式,必填,匹配给定名称的方法,可以使用通配符 * 作为名称模式的部分或全部,如果指定声明类型模式,请包含尾部 . 将其连接到名称模式组件
(param-pattern):(参数模式),必填,() 匹配无参、(..) 匹配任意个参数、(*) 匹配任意类型的一个参数、(*, String) 匹配两个参数,第一个任意类型,第二个String类型。
throws-pattern:异常模式,可选
举例:
// 匹配任意public方法
execution(public * *(..))
// 匹配任意以set开头的方法
execution(* set*(..))
// 匹配AccountService接口中定义的任意方法
execution(* com.xyz.service.AccountService.*(..))
// 匹配service包定义的任意方法
execution(* com.xyz.service.*.*(..))
// 匹配service或其子包中定义的任意方法
execution(* com.xyz.service..*.*(..))
②within
: 限制匹配确定类型的连接点 (方法执行的声明要在匹配类型当中)。
// 匹配service包中的任意连接点
within(com.xyz.service.*)
// 匹配service及其子包中的任意连接点
within(com.xyz.service..*)
③this
: 限制匹配bean引用(AOP代理)是给定类型实例的连接点,常用于绑定形式。
// 匹配代理实现AccountService接口的任意连接点
this(com.xyz.service.AccountService)
④target
: 限制匹配目标对象 (被代理的应用对象) 是给定类型实例的连接点,常用于绑定形式。
// 匹配目标对象实现AccountService接口的连接点
target(com.xyz.service.AccountService)
⑤args
: 限制匹配参数是给定类型实例的连接点,常用于绑定形式。
// 匹配有一个运行时传递参数是Serializable的连接点
args(java.io.Serializable)
⑥@target
: 限制匹配执行对象的类具有给定类型注解的连接点。
// 匹配目标对象的声明类型中具有@Transactional注解的连接点
@target(org.springframework.transaction.annotation.Transactional)
⑦@args
: 限制匹配传递的实参的运行时类型具有给定类型注解的连接点。
// 匹配一个传递参数的运行时类型具有@Classified注解的连接点
@args(com.xyz.security.Classified)
⑧@within
: 限制匹配给定注解中的类型的连接点。
// 匹配目标对象的声明类型中具有@Transactional注解的连接点
@within(org.springframework.transaction.annotation.Transactional)
⑨@annotation
: 限制匹配具有给定注解的连接点。
// 匹配具有@Transactional注解的正在执行的方法的连接点
@annotation(org.springframework.transaction.annotation.Transactional)
四、代理机制
1、AOP中使用的创建代理方式:
①JDK动态代理
②CGLIB(通用开源类定义库,在包spring-core中)
2、代理创建依据:
①如果代理对象至少实现了一个接口,使用JDK动态代理,并且目标类型实现的全部接口都将会被代理
②如果代理对象未实现任何接口,使用CGLIB创建代理
3、如果硬要使用CGLIB的话,需要考虑两个问题:
①使用CGLIB,由于final方法在运行时的子类中不能被重写,所以不能被通知
②从Spring4.0起,代理对象的构造方法不能重复调用,因为CGLIB代理实例是通过Objenesis类库创建的
五、AOP代理
①SpringAOP是基于代理的,掌握这个对于使用AOP切面是非常重要的
如下是一个普通的对象引用:
public class SimplePojo implements Pojo {
public void foo() {
// 直接调用this引用的bar()方法
this.bar();
}
public void bar() {
// some logic...
}
}
如果调用一个对象引用的方法,这个方法会在对象引用上被直接调用,如图所示:
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// 在pojo引用上直接调用方法
pojo.foo();
}
}
接下来,将客户端代码改为具有代理引用,如图所示:
public class Main {
public static void main(String[] args) {
// 为类创建代理工厂
ProxyFactory factory = new ProxyFactory(new SimplePojo());
// 被代理的接口
factory.addInterface(Pojo.class);
// 添加通知
factory.addAdvice(new RetryAdvice());
// 为类创建对象并给对象创建代理对象
Pojo pojo = (Pojo) factory.getProxy();
// 在代理对象上调用方法
pojo.foo();
}
}
这里要明白一个关键是客户端代码包含在具有代理引用的Main类的main(..)方法中,这意味着对该对象引用的方法调用是对代理的调用。代理可以委托给与特定方法调用相关的所有拦截器(通知)。
但是,一旦调用最终的目标对象,对象会调用它的任何方法,而没有通过代理调用,这具有重要意义,这意味着自调用会导致与方法调用关联的通知没有机会运行。例如:foo()方法中会被this引用调用bar()方法,但是没有代理调用,因此bar()方法不会被通知。
如何处理上述这种情况呢?
①重构代码使其不发生自调用,这是最好的、零侵入的
②将类中的逻辑绑定到AOP中,这显然增加了耦合度,如下所示:
public class SimplePojo implements Pojo {
public void foo() {
// 就像这样,将代码在AOP上下文中使用。。。糟糕透了!
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}
在创建代理时还需要增加一点配置
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
// 启用暴露代理
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
pojo.foo();
}
}
最后,必须注意的是,AspectJ没有这种自调用问题,因为它不是基于代理的AOP框架。
②通过@AspectJ自动创建代理
org.springframework.aop.aspectj.annotation.aspectjproxy工厂类为一个或多个@AspectJ切面通知的目标对象创建代理,基本使用如下:
// 创建一个为给定目标对象生成代理的工厂
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// 添加具有@AspectJ注解的切面类,这个方法可以根据多个不同切面进行多次调用
factory.addAspect(SecurityManager.class);
// 添加一个存在的切面实例, 但是所提供的对象的类必须是具有@AspectJ注解的切面
factory.addAspect(usageTracker);
// 获取代理对象
MyInterfaceType proxy = factory.getProxy();
六、在Spring应用中使用AspectJ
在这之前我们讨论的都是使用SpringAOP使用切面,它要求目标对象都必须是被SpringIoC容器所管理的,对于域对象并不起作用,因此,接下来我们使用AspectJ来完成SpringAOP不具备的功能。
使用AspectJ向Spring依赖注入域对象:spring-aspects.jar包中提供了注解驱动切面,可以使SpringIoC容器管理之外的任何对象依赖注入,即Spring应用上下文定义的bean之外的也可以接受切面通知,例:域对象,它们通常是通过new操作符创建,或者由ORM工具作为数据库查询的结果而创建的
如下所示:@Configurable注解标记了一个符合Spring-driven配置的类
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}
当用这种方式标记一个接口时,Spring通过使用与完全限定类型名同名的bean定义(通常是prototype作用域)来配置带注解类型(在本例中是Account)的新实例(com.xyz.myapp.domain.Account),如果要显式指定要使用的原型bean定义的名称,可以直接在注释中指定,Spring会找到一个名为account的bean定义,并使用该定义来配置新的account实例。
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}
可以使用@Configurable的autowire属性来避免指定专用bean定义,像这样@Configurable(autowire=Autowire.BY_TYPE)
or @Configurable(autowire=Autowire.BY_NAME)通过类型或者名称自动装配,作为替代方案,最好通过@Autowired或@Inject在字段或方法级别为@Configurable bean指定显式的、注解驱动的依赖项注入。可以使用属性 dependencyCheck
像这样@Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true) 对新创建和配置的对象引用开启Spring的依赖检查,开启依赖检查后,Spring在配置之后验证所有属性(不是基础类型或集合)是否都已设置。
单独使用@Configurable注解是不起作用的,还需要spring-aspects.jar中 AnnotationBeanConfigurerAspect对于存在
@Configurable注解起作用。本质上,切面所讲述的,“在初始化一个用@Configurable注解的类型的新对象之后,根据注解的属性使用Spring配置新创建的对象”。“这其中,初始化”指的是新实例化的对象(例如,使用new操作符实例化的对象)以及正在进行反序列化的可序列化对象,隐含的表名新对象的依赖关系是在构造方法执行后被注入的,如果需要在构造方法中使用之前注入的依赖项,可以像这样@Configurable(preConstruction = true) 使用@Configurable注解的preConstruction属性。对于AnnotationBeanConfigurerAspect使用的
问题,必须使用aspectjweaver织入带注解的类型,如果使用基于Java的配置,则可以将@EnableSpringConfigured添加到任何具有@configuration注解的类中,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
七、其他AspectJ的Spring切面
@Transactional注解是事务处理切面,在使用时必须要将注解添加到类或者方法上,不能是接口,因为接口上的注解不能被继承,并且类上添加这注解后只对公共方法执行默认语义的事务,方法上添加的注解会覆盖类上的注解的默认事务语义,任何可见方法都可以使用此注解,包括私有方法,这是非公共方法执行事务划分的唯一方式。
八、注意事项
推荐SpringAOP使用@AspectJ的注解形式,而不是XML配置文件
原因1:因为XML配置文件形式弊端是会使得声明(切面配置)和实现(切面Bean定义)分离(不符合DRY原则),但是注解方式可以将切面封装在一个位置;
原因2:注解方式可以将多个切点根据要求任意组合,这是XML做不到的。
注:DRY原则是指系统中的任何知识都应该有一个单一的、明确的、权威的表示。