AOP的概念、思想
OOP语言提供了类与类之间纵向的关系(继承、接口),而AOP补充了横向的关系(比如在不改变目标类中源代码的情况下给com.john.demo.dao包下所有类中以insert和update开头的方法添加事务管理)
面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。OOP中模块化的关键单元是类,而在AOP中,模块化单元是方面。方面实现了跨越多种类型和对象的关注点(例如事务管理)的模块化。(这些担忧在AOP文献中通常被称为“横切”问题。)
Spring的一个关键组件是AOP框架。虽然Spring IoC容器不依赖于AOP(意味着您不需要使用AOP),但AOP补充了Spring IoC以提供非常强大的中间件解决方案。
AOP在Spring Framework中用于:
- 提供声明性企业服务。最重要的此类服务是 声明式事务管理。
- 让用户实现自定义方面,补充他们使用AOP的OOP。
主要功能
日志记录,性能统计,安全控制,事务处理,异常处理等等。
主要意图
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
为什么用aop
- 就是为了方便,看一个国外很有名的大师说,编程的人都是“懒人”,因为他把自己做的事情都让程序做了。用了aop能让你少写很多代码,这点就够充分了吧
- 就是为了更清晰的逻辑,可以让你的业务逻辑去关注自己本身的业务,而不去想一些其他的事情,这些其他的事情包括:安全,事物,日志等。
AOP术语
切面(Aspect)
我们将自己需要插入到目标业务逻辑中的代码模块化, 通过AOP使之可以横切多个类的模块,称之为切面。
在Spring AOP配置中切面通常包含三部分:
- 切面模块本身
- 通知
- 切入点
示例:
<!-- 目标业务逻辑代码 -->
<bean id="calc" class="com.lanou3g.spring.simple.calc.CalcImpl"/>
<!-- 切面模块化对象(代表我们要附加到原始业务逻辑中的代码) -->
<bean id="calcAspect" class="com.lanou3g.spring.simple.calc.CalcAspect" />
<!-- 示例说明: 将切面calcAspect中的代码插入到calc原始业务代码中 -->
<aop:config>
<!-- 定义公用的切入点表达式,如果aspect中有多个通知,都可以通过pointcut-ref复用 -->
<aop:pointcut id="all_calc_method" expression="execution(* com.lanou3g.spring.simple.calc.CalcImpl.*(..))" />
<aop:aspect ref="calcAspect">
<!-- 切面包含的通知(什么时间)、切入点(什么地点) -->
<aop:around method="computeTime" pointcut-ref="all_calc_method" />
</aop:aspect>
</aop:config>
切入点 (Pointcut)
在 Spring AOP 中,需要使用 AspectJ 的切点表达式来定义切点。
AspectJ 指示器 | 描述 |
---|---|
execution () | 用于匹配连接点的执行方法 最常用 |
args () | 限制连接点的指定参数为指定类型的执行方法 |
@args () | 限制连接点匹配参数类型由指定注解标注的执行方法 |
this () | 限制连接点匹配 AOP 代理的 Bean 引用为指定类型的类 |
target () | 限制连接点匹配特定的执行对象,目标对象是指定的类型 |
@target () | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型注解 |
within() | 限制连接点匹配指定类型,比如哪个包下,或哪个类里面 |
@within() | 限制连接点匹配指定注释所标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里) |
@annotation | 限制匹配带有指定注释的连接点 |
通知(Advice)
环绕通知(around)
- 在目标方法执行前、后被通知, 可以获取连接点对象(ProceedingJoinPoint, 该对象可以获取被拦截方法的签名、参数、返回值、包括调用与否)
- 该方法的返回值,即代表了真正业务逻辑代码的返回值
- 可以选择终止或正常执行目标方法
前置通知(before)
在目标方法调用前通知切面, 什么参数也无法获取。也不能终止目标方法执行
后置(返回值)通知(after returning)
只有在目标方法 正常 执行结束后才会通知, 在通知方法中可以获取到方法的返回值
后置(最终)通知 (after)
在目标方法执行结束后通知切面, 什么参数也无法获取。无论目标方法是正常执行结束还是抛出异常终止,都会被通知
异常通知(after throwing)
只有在目标方法 出现异常 才会通知, 在通知方法中可以获取到抛出的异常信息
五种通知的示例
见下面的 XML方式示例(五种通知)和 注解方式示例(五种通知)
连接点(JoinPoint)
spring仅支持方法连接点
-
因为 Spring 基于动态代理,所以 Spring 只支持方法连接点。
-
Spring 缺失对字段连接点的支持,无法让我们更加细粒度的通知,例如拦截对象字段的修改
-
Spring 缺失对构造器连接点支持,我发在 Bean 创建时候进行通知。
连接点有很多种,比如方法执行期间(开始执行、执行结束、抛出异常)、字段修饰符、字段值被更改…
在Spring AOP中只支持方法连接点(因为Spring AOP底层是通过动态代理实现的)。
连接点与切入点的关系可以简单理解为: 切入点一定是连接点, 连接点不一定是切入点。
织入(Weaver)
织入的过程其实就是Spring AOP帮我们把切面中的代码织入到目标代码中的过程。
使用步骤
添加依赖包
在mavan工程中添加下面的依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
xml方式配置
AOP 配置元素 | 描述 |
---|---|
aop : advisor | 定义 AOP 通知器 |
aop : after | 定义 AOP 后置通知(不管被通知方法是否执行成功) |
aop : after-returing | 定义 AOP after-returing 通知 |
aop : after-throwing | 定义 AOP after-throwing 通知 |
aop : around | 定义 AOP 环绕通知 |
aop : aspect | 定义切面 |
aop : aspectj-autoproxy | 启动 @AspectJ 注解驱动的切面 |
aop : before | 定义 AOP 前置通知 |
aop : config | 顶层的 AOP 配置元素,大多数 aop : * 元素必须包含在 元素内 |
aop : declare-parents | 为被通知的对象引入额外接口,并透明的实现 |
aop : pointcut | 定义切点 |
加入aop schema
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
定义切面类,并配置到applicationContext中
<bean id="calcAspect" class="com.lanou3g.spring.simple.calc.CalcAspect" />
CalcAspect.java
public class CalcAspect {
/**
* 计算方法耗时
* 环绕通知
* @param joinPoint 代表连接点对象,该对象可以获取被代理方法的所有信息
*/
public Object aroundM(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取连接点代表的方法的签名
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
Object[] args = joinPoint.getArgs();
System.out.println("[aroundM] ---- 目标方法"+methodName+"("+ Arrays.toString(args)+")开始执行");
long start = System.currentTimeMillis();
// 调用目标方法
Object retVal = joinPoint.proceed();
// 插入我们自己的逻辑代码
long timer = System.currentTimeMillis() - start;
System.out.println("[aroundM] ---- 目标方法["+methodName+"("+ Arrays.toString(args)+")]执行结束,返回值: "+retVal+", 耗时: " + timer + "ms.");
// 正常返回目标方法的返回值
return retVal;
}
public void beforeM() {
System.out.println("[beforeM] ---- 目标方法开始执行");
}
public void afterReturningM(Object retVal) {
System.out.println("[afterReturningM] ---- 目标方法执行结束,返回值: " + retVal);
}
public void afterFinallyM() {
System.out.println("[afterFinallyM] ---- 方法执行结束");
}
public void afterThrowing(Throwable throwable) {
System.out.println("[afterThrowing] ---- 方法执行出错", throwable);
}
}
定义 < aop:config >
<aop:config>
<aop:pointcut id="all_calc_method" expression="execution(* com.lanou3g.spring.simple.calc.CalcImpl.*(..))" />
<aop:aspect ref="calcAspect">
<aop:around method="computeTime" pointcut-ref="all_calc_method" />
</aop:aspect>
</aop:config>
引用切面bean
<bean id="calcAspect" class="com.lanou3g.spring.simple.CalcAspect" />
定义切入点表达式
在 Spring AOP 中,需要使用 AspectJ 的切点表达式来定义切点。
AspectJ 指示器 | 描述 |
---|---|
execution () | 用于匹配连接点的执行方法 最常用 |
args () | 限制连接点的指定参数为指定类型的执行方法 |
@args () | 限制连接点匹配参数类型由指定注解标注的执行方法 |
this () | 限制连接点匹配 AOP 代理的 Bean 引用为指定类型的类 |
target () | 限制连接点匹配特定的执行对象,目标对象是指定的类型 |
@target () | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型注解 |
within() | 限制连接点匹配指定类型,比如哪个包下,或哪个类里面 |
@within() | 限制连接点匹配指定注释所标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里) |
@annotation | 限制匹配带有指定注释的连接点 |
切入点表达式语法:
语法一:单个表达式
切入点表达式语法
<aop:pointcut id="all_method" expression="execution(* com.lanou3g.spring.simple.CalcImpl.*(..))" />
- execution表达式以 * 号开头,标识了我们不关心的方法返回值的类型。
- 后我们指定了权限定类名和方法名。(类名和方法名都可以用号通配符代替)
- 对于方法的参数列表,使用(…)标识切点选择任意的 play( ) 方法,无论入参是什么。
语法二:多个表达式组合
切入点表达式语法
<aop:pointcut id="all_method" expression="execution(* com.lanou3g.spring.simple.CalcImpl.*(..)) && within(com.lanou3g.spring)" />
这里我们使用 &&(在xml中是关键字,需要转义) 将 execution( ) 和 within( ) 连接起来,形成的 and 关系。同理也可以使用 || 或关系、!非关系
定义通知
<aop:around method="computeTime" pointcut-ref="all_calc_method" />
通知类型:引入切入点表达式、织入切面bean中的方法
<bean id="calcAspect" class="com.lanou3g.spring.simple.CalcAspect" />
<!-- 示例说明: 将切面calcAspect中的代码插入到calc原始业务代码中 -->
<aop:config>
<aop:pointcut id="all_method" expression="execution(* com.lanou3g.spring.simple.CalcImpl.*(..))" />
<aop:aspect ref="calcAspect">
<!-- 环绕通知(around) -->
<aop:around method="aroundMethod" pointcut-ref="all_method" />
<!-- 前置通知(before) -->
<aop:before method="beforeMethod" pointcut-ref="all_method" />
<!-- 后置(返回值)通知(after returning) -->
<aop:after-returning method="afterReturningMethod" pointcut-ref="all_method" returning="retVal" />
<!-- 后置(最终)通知 (after) -->
<aop:after method="afterFinallyMethod" pointcut-ref="all_method" />
<!-- 异常通知(after throwing) -->
<aop:after-throwing method="afterThrowingMethod" pointcut-ref="all_method" throwing="throwable" />
</aop:aspect>
</aop:config>
XML方式示例(五种通知)
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 目标业务逻辑代码 -->
<bean id="calc" class="com.lanou3g.spring.simple.CalcImpl" />
<!-- 切面模块化对象(代表我们要附加到原始业务逻辑中的代码) -->
<bean id="calcAspect" class="com.lanou3g.spring.simple.CalcAspect" />
<!-- 示例说明: 将切面calcAspect中的代码插入到calc原始业务代码中 -->
<aop:config>
<aop:pointcut id="all_method" expression="execution(* com.lanou3g.spring.simple.CalcImpl.*(..))" />
<aop:aspect ref="calcAspect">
<!-- 环绕通知(around) -->
<aop:around method="aroundMethod" pointcut-ref="all_method" />
<!-- 前置通知(before) -->
<aop:before method="beforeMethod" pointcut-ref="all_method" />
<!-- 后置(返回值)通知(after returning) -->
<aop:after-returning method="afterReturningMethod" pointcut-ref="all_method" returning="retVal" />
<!-- 后置(最终)通知 (after) -->
<aop:after method="afterFinallyMethod" pointcut-ref="all_method" />
<!-- 异常通知(after throwing) -->
<aop:after-throwing method="afterThrowingMethod" pointcut-ref="all_method" throwing="throwable" />
</aop:aspect>
</aop:config>
</beans>
切面模块化对象
public class CalcAspect {
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取连接点代表的方法的签名
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
Object[] args = joinPoint.getArgs();
System.out.println("[aroundMethod] ---- 目标方法" + methodName + "(" + Arrays.toString(args) + ")开始执行");
long start = System.currentTimeMillis();
// 调用目标方法
Object retVal = joinPoint.proceed();
// 插入我们自己的逻辑代码
long timer = System.currentTimeMillis() - start;
System.out.println("[aroundMethod] ---- 目标方法[" + methodName + "(" + Arrays.toString(args) + ")]执行结束,返回值: " + retVal + ", 耗时: " + timer + "ms.");
// 正常返回目标方法的返回值
return retVal;
}
public void beforeMethod() {
System.out.println("[beforeMethod] ---- 目标方法开始执行");
}
public void afterReturningMethod(Object retVal) {
System.out.println("[afterReturningMethod] ---- 目标方法执行结束,返回值: " + retVal);
}
public void afterFinallyMethod() {
System.out.println("[afterFinallyMethod] ---- 方法执行结束");
}
public void afterThrowingMethod(Throwable throwable) {
System.out.println("[afterThrowingMethod] ---- 方法执行出错" + throwable);
}
}
目标业务逻辑代码
// 逻辑业务接口
public interface Calc {
public int sum(int num1, int num2);
public int product(int num1, int num2);
}
// 接口实现类
public class CalcImpl implements Calc {
@Override
public int sum(int num1, int num2) {
return num1 + num2;
}
@Override
public int product(int num1, int num2) {
// 模拟方法执行报错,便于看到after-throwing通知
int ret = 9 / 0;
return num1 * num2;
}
}
// 程序运行入口
import com.lanou3g.spring.simple.Calc;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Calc calc = ctx.getBean(Calc.class);
int sum = calc.sum(527, 682);
System.out.println("控制台输出:"+ sum);
int product = calc.product(5, 89);
System.out.println("控制台输出:" + product);
}
}
控制台输出结果
//程序正常运行结果
[aroundMethod] ---- 目标方法sum([527, 682])开始执行
[beforeMethod] ---- 目标方法开始执行
[aroundMethod] ---- 目标方法[sum([527, 682])]执行结束,返回值: 1209, 耗时: 1ms.
[afterReturningMethod] ---- 目标方法执行结束,返回值: 1209
[afterFinallyMethod] ---- 方法执行结束
控制台输出:1209
//模拟异常运行结果
[aroundMethod] ---- 目标方法product([5, 89])开始执行
[beforeMethod] ---- 目标方法开始执行
[afterFinallyMethod] ---- 方法执行结束
[afterThrowingMethod] ---- 方法执行出错java.lang.ArithmeticException: / by zero
注解方式
开启自动织入支持
在xml中开启
<!-- 开启aop注解支持 -->
<aop:aspectj-autoproxy />
<!-- 开启注解支持,同时强制指定代理机制为cglib -->
<aop:aspectj-autoproxy proxy-target-class="true" />
通过注解开启
@Configuration
// 开启注解支持,同时强制指定代理机制为cglib(注:一般不指定)
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class MyAOPConf {}
定义切面类,添加@Aspect注解、@Component注解
由于@Aspect注解没有让Spring作为组件bean扫描的能力,所以我们需要额外添加@Component注解
定义切入点
/**
* 通过@Pointcut注解定义切入点表达式
* 此处表达式含义:拦截com.lanou3g.spring.simple.say包下所有类(包括子包中所有类)中的所有方法
*/
@Pointcut("execution(* com.lanou3g.spring.simple.say..*.*(..))")
public void say_all_method() {}
定义通知
@AfterReturning(pointcut = "com.lanou3g.spring.GlobalPointcut.say_all_method()", returning = "ret")
public Object afterRM(Object ret) {
log.debug("[afterRM] 返回值: " + ret);
return ret;
}
注解方式示例(五种通知)
定义系统中所有用到的切入点表达式
/**
* 定义系统中所有用到的切入点表达式
*/
@Component
public class GlobalPointcut {
/**
* 通过@Pointcut注解定义切入点表达式
* 此处表达式含义:拦截com.lanou3g.spring.annotation包下所有类(包括子包中所有类)中的所有方法
*/
@Pointcut("execution(* com.lanou3g.spring.annotation.*.*(..))")
public void all_method() {}
}
切面模块化对象
@Component
@Aspect
public class ClacAspectAnnotation {
@Around("com.lanou3g.spring.annotation.GlobalPointcut.all_method()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取连接点代表的方法的签名
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
Object[] args = joinPoint.getArgs();
System.out.println("[aroundMethod] ---- 目标方法" + methodName + "(" + Arrays.toString(args) + ")开始执行");
long start = System.currentTimeMillis();
// 调用目标方法
Object retVal = joinPoint.proceed();
// 插入我们自己的逻辑代码
long timer = System.currentTimeMillis() - start;
System.out.println("[aroundMethod] ---- 目标方法[" + methodName + "(" + Arrays.toString(args) + ")]执行结束,返回值: " + retVal + ", 耗时: " + timer + "ms.");
// 正常返回目标方法的返回值
return retVal;
}
@Before("com.lanou3g.spring.annotation.GlobalPointcut.all_method()")
public void beforeMethod() {
System.out.println("[beforeMethod] ---- 目标方法开始执行");
}
@AfterReturning(value = "com.lanou3g.spring.annotation.GlobalPointcut.all_method()",returning = "retVal")
public void afterReturningMethod(Object retVal) {
System.out.println("[afterReturningMethod] ---- 目标方法执行结束,返回值: " + retVal);
}
@AfterThrowing(value = "com.lanou3g.spring.annotation.GlobalPointcut.all_method()",throwing = "throwable")
public void afterThrowingMethod(Throwable throwable) {
System.out.println("[afterThrowingMethod] ---- 方法执行出错" + throwable);
}
@After("com.lanou3g.spring.annotation.GlobalPointcut.all_method()")
public void afterFinallyMethod() {
System.out.println("[afterFinallyMethod] ---- 方法执行结束");
}
}
目标业务逻辑代码
// 逻辑业务接口
public interface CalcAnnotation {
int sum(int num1, int num2);
int product(int num1, int num2);
}
// 接口实现类
@Component
public class CalcAnnotationImpl implements CalcAnnotation {
@Override
public int sum(int num1, int num2) {
return num1 + num2;
}
@Override
public int product(int num1, int num2) {
// 模拟方法执行报错,便于看到after-throwing通知
int ret = 9 / 0;
return num1 * num2;
}
}
// 程序运行入口
@Configuration
@ComponentScan(basePackages = "com.lanou3g.spring.annotation")
@EnableAspectJAutoProxy
public class AppAnnotation {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppAnnotation.class);
CalcAnnotation calcAnnotation = ctx.getBean(CalcAnnotation.class);
int sum = calcAnnotation.sum(213,3213);
System.out.println("控制台输出:"+ sum);
int product = calcAnnotation.product(5, 89);
System.out.println("控制台输出:" + product);
}
}
控制台输出结果
//程序正常运行结果
[aroundMethod] ---- 目标方法sum([213, 3213])开始执行
[beforeMethod] ---- 目标方法开始执行
[aroundMethod] ---- 目标方法[sum([213, 3213])]执行结束,返回值: 3426, 耗时: 1ms.
[afterFinallyMethod] ---- 方法执行结束
[afterReturningMethod] ---- 目标方法执行结束,返回值: 3426
控制台输出:3426
//模拟异常运行结果
[aroundMethod] ---- 目标方法product([5, 89])开始执行
[beforeMethod] ---- 目标方法开始执行
[afterFinallyMethod] ---- 方法执行结束
[afterThrowingMethod] ---- 方法执行出错java.lang.ArithmeticException: / by zero