Spring AOP详解
一、AOP—另一种编程思想
1.1 什么是AOP
AOP(Aspect Orient Programming),直译过来就是面向切面编程,AOP是一种思想,是面向对象编程(OOP)的一种补充,面向对象编程将程序抽象为各个层次的对象,而面向切面编程是将程序抽象成各个切面。
所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块
1.2 为什么需要AOP
OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用
AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
1.3 AOP实现分类
AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码。
按照 AOP 框架修改源代码的时机,可以将其分为两类:
- 静态AOP实现:AOP框架在编译阶段对程序源代码进行修改,生成了静态的AOP代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ。
- 动态 AOP 实现, AOP 框架在运行阶段对动态生成代理对象(在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类),如 SpringAOP。
1.3.1 静态代理
首先,我们创建一个Person接口。这个接口就是学生(被代理类),和班长(代理类)的公共接口,他们都有交作业的行为。这样,学生交作业就可以让班长来代理执行
/**
* 创建person接口
*/
public interface Person {
//交作业
void giveTask();
}
Student类实现Person接口,Student可以具体实施交作业这个行为.
public class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
public void giveTask() {
System.out.println(name + "交语文作业");
}
}
StudentsProxy类,这个类也实现了Person接口,但是还另外持有一个学生类对象,那么他可以代理学生类对象执行交作业的行为
/**
* 学生代理类,也实现了Person接口,保存一个学生实体,这样就可以代理学生产生行为
*/
public class StudentsProxy implements Person{
//被代理的学生
Student stu;
public StudentsProxy(Person stu) {
// 只代理学生对象
if(stu.getClass() == Student.class) {
this.stu = (Student)stu;
}
}
//代理交作业,调用被代理学生的交作业的行为
public void giveTask() {
stu.giveTask();
}
}
下面测试一下,看代理模式如何使用
public class StaticProxyTest {
public static void main(String[] args) {
//被代理的学生林浅,他的作业上交有代理对象monitor完成
Person linqian = new Student("林浅");
//生成代理对象,并将林浅传给代理对象
Person monitor = new StudentsProxy(linqian);
//班长代理交作业
monitor.giveTask();
}
}
运行结果:
这里并没有直接通过林浅(被代理对象)来执行交作业的行为,而是通过班长(代理对象)来代理执行了。这就是代理模式。代理模式就是在访问实际对象时引入一定程度的间接性,这里的间接性就是指不直接调用实际对象的方法,那么我们在代理过程中就可以加上一些其他用途。比如班长在帮林浅交作业的时候想告诉老师最近林浅的进步很大,就可以轻松的通过代理模式办到。在代理类的交作业之前加入方法即可。这个优点就可以运用在spring中的AOP,我们能在一个切点之前执行一些操作,在一个切点之后执行一些操作,这个切点就是一个个方法。这些方法所在类肯定就是被代理了,在代理过程中切入了一些其他操作。
1.3.2 动态代理
动态代理和静态代理的区别是,静态代理的的代理类是我们自己定义好的,在程序运行之前就已经编译完成,但是动态代理的代理类是在程序运行时创建的。相比于静态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。比如我们想在每个代理方法之前都加一个处理方法,我们上面的例子中只有一个代理方法,如果还有很多的代理方法,就太麻烦了,我们来看下动态代理是怎么去实现的。
首先还是定义一个Person接口:
public interface Person {
//交作业
void giveTask();
}
接下来是创建需要被代理的实际类,也就是学生类:
public class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
public void giveTask() {
System.out.println(name + "交语文作业");
}
}
创建StuInvocationHandler类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。
public class StuInvocationHandler<T> implements InvocationHandler {
//invocationHandler持有的被代理对象
T target;
public StuInvocationHandler(T target) {
this.target = target;
}
/**
* proxy:代表动态代理对象
* method:代表正在执行的方法
* args:代表调用目标方法时传入的实参
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理执行" +method.getName() + "方法");
Object result = method.invoke(target, args);
return result;
}
}
那么接下来我们就可以具体的创建代理对象了。
/**
* 代理类
*/
public class ProxyTest {
public static void main(String[] args) {
//创建一个实例对象,这个对象是被代理的对象
Person linqian = new Student("林浅");
//创建一个与代理对象相关联的InvocationHandler
InvocationHandler stuHandler = new StuInvocationHandler<Person>(linqian);
//创建一个代理对象stuProxy来代理linqian,代理对象的每个执行方法都会替换执行Invocation中的invoke方法
Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);
//代理执行交作业的方法
stuProxy.giveTask();
}
}
我们执行代理测试类,首先我们创建了一个需要被代理的学生林浅,将林浅传入stuHandler中,我们在创建代理对象stuProxy时,将stuHandler作为参数,那么所有执行代理对象的方法都会被替换成执行invoke方法,也就是说,最后执行的是StuInvocationHandler中的invoke方法。所以在看到下面的运行结果也就理所当然了。
二、AOP 术语
- 切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用基于模式或者基于@Aspect注解的方式来实现。
- 连接点(Joinpoint): 在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。
- 通知/增强(Advice): 在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。spring aop advice的类型:
1、前置通知(before advice),在目标方法执行之前执行。
2、返回后通知(after returning advice),在方法正常执行结束之后的通知,可以访问到方法的返回值。
3、抛出异常后通知(after throwing advice),在目标方法出现异常时执行的代码,可以访问到异常对象,且可以指定出现特定异常执行此方法。
4、后置通知:(after[finally] advice),在目标方法执行之后执行(无论是否发生异常)。
5、环绕通知:(around advice),可以实现上述所有功能。 - 切入点(Pointcut): 指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。 Spring定义了Pointcut接口,用来组合MethodMatcher和ClassFilter,可以通过名字很清楚的理解, MethodMatcher是用来检查目标类的方法是否可以被应用此通知,而ClassFilter是用来检查Pointcut是否应该应用到目标类上
- 引入(Introduction): 添加方法或字段到被通知的类。 Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified接口,来简化缓存。Spring中要使用Introduction, 可有通过DelegatingIntroductionInterceptor来实现通知,通过DefaultIntroductionAdvisor来配置Advice和代理类要实现的接口
- 目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO
- AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
- 织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
三、AOP的实现步骤
公式:AOP=(切面)=通知方法(5种)+切入点表达式(4种)
通知方法:
1.@before通知:在执行目标方法之前执行
2.@after通知:在执行目标方法之后执行
3.@afterReturning通知:无论什么时候程序执行完成之后都要执行的通知
4.@afterThrowing通知:在目标方法执行之后报错时执行
(上述四大类型通知,不能控制目标方法是否执行。一般用来记录程序的执行状态。一般应用与监控的操作。(不改变程序运行的轨迹)
5.@around通知:在目标方法执行前后执行(环绕通知可控制目标方法是否执行,控制程序的执行的轨迹)
切入点表达式
1.@bean(“beanId”):bean:交给spring容器管理的对象,粒度:粗粒度 按bean匹配 当前bean中的方法都会执行通知
2.@within(“包名.类名”):粒度:粗粒度 可以匹配多个类
3.@execution("返回值类型 包名.类名.方法名(参数列表: 细粒度:方法参数级别
4.@annotation(“包名.类名”):细粒度:按照注解匹配
package com.jt.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component//交给spring容器管理
public class CacheAOP {
//@Pointcut("bean(itemCatServiceImpl)")
//@Pointcut("within(com.jt.service.ItemCatServiceImpl)")
//@Pointcut("within(com.jt.service.*)")//.*一级包目录,..*所有子孙后代
@Pointcut("execution(* com.jt.service..*.add*(..))")
public void pointCut(){
}
@Before("pointCut()")
public void before(){
System.out.println("我是before通知");
}
}
四、AOP 实例
4.1 XML中声明AOP的常用元素
AOP元素 | 用途 |
---|---|
aop:advisor | 定义aop通知器 |
aop:after | 定义一个后置通知(不管目标方法是否执行成功) |
aop:after-returning | 定义aop的返回通知 |
aop:after-throwing | 定义aop异常通知 |
aop:around | 定义环绕通知 |
aop:aspect | 定义一个切面 |
aop:aspectj-autoproxy | 启动@aspectj注解驱动的切面 |
aop:before | 定义一个aop前置通知 |
aop:config | 顶层aop配置元素,大多数的aop:*都必须包含在aop:config元素内 |
aop:declare-parents | 以透明的方式为被通知的对象引入额外的接口 |
aop:pointcut | 定义一个切点 |
4.2 基于xml的配置方式
applicationContext.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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<!-- 将类交给容器去管理 -->
<bean id="userService" class="cn.com.demo.service.UserService"/>
<!-- 将类交给容器去管理 -->
<bean id="logAopDemo" class="cn.com.demo.aop.LogAop"/>
<!-- 面向切面编程 -->
<aop:config>
<aop:aspect ref="logAopDemo">
<!-- 定义切点 -->
<aop:pointcut expression="execution(* *..*(..))" id="pointCut"/>
<!-- 声明前置通知 (在切点方法被执行前调用)-->
<aop:before method="beforeAdvice" pointcut-ref="pointCut"/>
<!-- 声明后置通知 (在切点方法被执行后调用)-->
<aop:after method="afterAdvice" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
</beans>
注:aop:aspect 子节点还可配置
<aop:config>
<aop:aspect ref="logAopDemo">
<!-- 定义切点 -->
<aop:pointcut expression="execution(* *..*(..))" id="pointCut"/>
<!-- 声明前置通知 (在切点方法被执行前调用)-->
<aop:before method="beforeAdvice" pointcut-ref="pointCut"/>
<!-- 声明后置通知 (在切点方法被执行后调用)-->
<aop:after method="afterAdvice" pointcut-ref="pointCut"/>
<aop:after-returning method="aferReturning" pointcut-ref="pointCut"/>
<aop:after-throwing method="aferThrowing" pointcut-ref="pointCut"/>
<aop:around method="around" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
4.3 基于注解的配置方式
applicationContext.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:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<bean id="userService" class="cn.com.demo.service.UserService"/>
<!-- 启用注释驱动自动注入 -->
<context:annotation-config/>
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="cn.com.demo"></context:component-scan>
<!-- 开启aop注解方式,此步骤不能少,这样java类中的aop注解才会生效 -->
<aop:aspectj-autoproxy/>
</beans>