Spring 官方解释:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop
面向切面编程(Aspect-oriented Programming ,AOP)通过提供另一种思考程序设计结构的方式来补充面向对象程序设计(Object-oriented Programming,OOP)。面向对象编程中,模块化的关键单元是类,而在面向切面编程中,模块化的单元是方面。方面支持对关注点的模块化(比如事务管理)跨多个类型和对象。(这种关注在 AOP 文献中通常被称为 “跨领域”关注)
AOP 是 Spring 的一个关键组件,尽管 Spring IoC 容器不依赖于 AOP(意味着你不需要用AOP的话,可以不使用),但 AOP 是对 Spring IoC 的补充,提供了功能强大的中间件解决方案。
也可以去看看:百度百科
相关术语:
术语 | 描述 |
---|---|
连接点(Join point) | 指的是那些被拦截到的点。在 Spring 中,这些点指的是方法,因为 Spring 只支持方法类型的连接点。 |
切入点(Point Cut) | 指的是我们要对哪些 Join point 进行拦截的定义。被增强的连接点就叫做切入点,切入点一定是连接点,连接点不一定是切入点。 |
通知 / 增强(Advice) | 指拦截到 Join Point 之后要执行的操作。类型:前置通知,后置通知,异常通知,最终通知,环绕通知。 |
引介(Introduction) | 是一种特殊的通知,可以为类动态地添加一些方法 或 Filed。 |
目标对象(Target object) | 代理的目标对象。 |
织入(Weaving) | 指把增强应用到的目标对象来创建新的代理对象的过程。 spring 采用 动态织入,而 AspectJ 采用编译期织入和类装载期织入。 |
AOP 代理(AOP proxy) | 一个类被 AOP 织入增强后,就产生了一个结果代理类。 |
切面(Aspect) | 是切入点和通知 / 引介 的结合。建立切入点方法和通知方法在调用执行时的对应关系就是一个切面 |
AOP 可以对业务逻辑得到各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
通俗描述:不修改源代码的条件下,在主干功能中添加新功能。
案例解释:
现在有一个账户服务类( AccountService
),类中有一个转账的方法( transferAccounts
)。顾名思义,转账操作就是要将一个账户的钱转给另一个账户,这个过程需要查询两次,修改余额两次。如果一个账户在转账的过程中出现异常,导致转账人的钱减少了,而收款人的钱没有到账的情况。这种情况可以通过添加事务来解决,可以在源代码上直接做处理,但如果有很多个类似的方法呢?每个方法都加上事务管理,代码无疑会变得十分冗余,这时就可以使用一个代理类(AccountProxy
),将 AccountService
类中所有需要事务管理的方法都拦截放到添加事务管理的方法中,这样就可以做的不修改源代码的条件下,增强功能。这就是 AOP 的过程。
在 Spring 中用 AOP 术语匹配以上案例:
AOP 底层使用到了动态代理模式:
菜鸟教程-代理模式
简单理解代理模式:用户购买电脑不是到生产厂家处购买,而是到经销商处购买,经销商又从生产厂家取货。倘若用户电脑出问题了,找的是经销商的售后,经销商售后要么自己处理,要么返还给生产厂家处理,这个过程就叫做代理。
这是基于接口的动态代理方式,是由 JDK 提供。
使用类和方法:java.lang.reflect.Proxy.newProxyInstance
要求被代理对象必须实现至少一个接口,否则出现以下异常:
java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to xxx
可参考:Proxy API介绍
newProxyInstance
参数解读:
ClassLoader loader
:代理类的类加载器,使用 xxx.class.getClassLoader() 获取类加载器,xxx表示代理对象。Class>[] interfaces
: 被代理对象的所有接口,使用 xxx.class.getInterfaces() 获取被代理对象的所有接口,xxx 表示被代理对象。目的是让代理对象与被代理对象有相同的方法。InvocationHandler h
: 调用代理对象方法的控制器接口,可以在这个接口的实现类中写增强方法的代码。里面有一个唯一的方法 invoke()
需要被重写。通常是匿名内部类的形式。nvocationHandler
接口中的 invoke
方法:用于控制代理实例上的方法调用和返回结果。
invoke
方法参数解读:
Object proxy
: 代理对象,方法被调用的地方。Method method
: 被代理对象中的方法,可以使用 method.getName()
方法获取当前执行方法的名称。Object[] args
: 被代理对象中方法的参数列表,可使用索引的方式获取参数,如args[0]
表示当前执行方法的第一个参数。代理过程演示:
1 创建接口,定义相关方法
//被代理对象,模拟播放人类从匍匐到奔跑的过程的视频
public interface Person {
public void walk();
}
2 创建接口实现类并实现方法
public class PersonImpl implements Person{
public void walk(){
System.out.println("视频播放:人类行走过程。。。");
// int a = 1/0; //手动制造异常,默认在观看人类行走时产生疑惑
}
}
3 使用 Proxy 类创建接口代理对象。
public class PersonProxy {
// 创建被代理对象
PersonImpl personImpl = new PersonImpl();
// 返回代理对象的方法
public Object personProxy() {
return Proxy.newProxyInstance(
PersonProxy.class.getClassLoader(),
PersonImpl.class.getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(proxy.getClass());
try {
Object obj = null;
System.out.println("视频播放:人类匍匐过程。。。"); // 被增强方法执行前的动作
obj = method.invoke(personImpl, args); // 被增强方法被明确调用,保证原有的功能
System.out.println("视频播放:人类奔跑奔跑。。。"); // 被增强方法执行后执行的动作
return obj;
} catch (Exception e) {
System.out.println("观众产生疑问。。。"); //被增强方法执行过程中出现异常执行的动作
throw new RuntimeException(e);
} finally {
System.out.println("视频播放结束。。。"); // 无论方法是否正常执行,都会执行的动作
}
}
});
}
}
4 测试方法
public static void main(String[] args){
PersonProxy pp = new PersonProxy();
Person p = (Person) pp.personProxy();
p.walk();
}
打印结果:
class com.sun.proxy.$Proxy0
视频播放:人类匍匐过程。。。
视频播放:人类行走过程。。。
视频播放:人类奔跑奔跑。。。
视频播放结束。。。
这是基于继承方式的动态代理,是由第三方 cglib
库提供。
使用的类和方法:cglib.proxy.Enhancer.create
它要求被代理对象不能是最终类(无法继承),否则出现以下异常;
java.lang.IllegalArgumentException: Cannot subclass final class xxx
可参考以下博客:
CGLIB原理及实现机制
CGLib动态代理的介绍及用法(单回调、多回调、不处理、固定值、懒加载)
create
方法参数解读:
Class type
:被代理对象的字节码文件。Callback callback
:回调函数,可以使用这个接口的子接口提供的实现类中写增强方法的代码。与 JDK 动态代理中的 InvocationHandler
大同小异,这里是用于拦截方法的,所以用到的是方法拦截接口:MethodInterceptor
,接口中有唯一且需要被重写方法 intercept()
。intercept()
方法参数解读:
Object o
:被代理对象。Method method
:拦截到的方法Object[] objects
:被拦截方法的参数数组,基本类型会被包装成包装类型。MethodProxy methodProxy
:用于调用父类中未被拦截的方法的代理。代理过程演示:
1 创建被代理类
public class Person {
public void walk() {
System.out.println("视频播放:人类行走前进中。。。");
// int a = 1/0; //手动制造异常,默认在观看人类行走时产生疑惑
}
}
2 创建代理对象
public class PersonProxy {
Person person = new Person();
public Object getPersonProxy(){
return Enhancer.create(person.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println(o.getClass());
try{
Object obj = null;
System.out.println("视频播放:人类匍匐前进中。。。"); // 被增强方法执行前的动作
obj = method.invoke(person,objects); // 被增强方法被明确调用,保证原有的功能
System.out.println("视频播放:人类奔跑前进中。。。"); // 被增强方法执行后执行的动作
return obj;
}catch (Exception e){
System.out.println("观众产生疑问。。。"); //被增强方法执行过程中出现异常执行的动作
throw new RuntimeException(e);
}finally {
System.out.println("视频播放结束。。。"); // 无论方法是否正常执行,都会执行的动作
}
}
});
}
}
3 测试方法
public static void main(String[] args) {
PersonProxy pp = new PersonProxy();
Person p = (Person)pp.getPersonProxy();
p.walk();
}
打印结果:
class main.a_proxy.cglib.Person$$EnhancerByCGLIB$$7f3ac652
视频播放:人类匍匐前进中。。。
视频播放:人类行走前进中。。。
视频播放:人类奔跑前进中。。。
视频播放结束。。。
以上动态代理内容作为了解即可,下面用 Spring 中的 AOP 完成上面代理的过程。
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.8.RELEASEversion>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.9.5version>
dependency>
public interface AccountService {
void saveAccount(); //模拟保存
void updateAccount(int id); //模拟更新
int deleteAccount(); //模拟删除
}
public class AccountServiceImpl implements AccountService {
@Override
public void saveAccount() {
System.out.println("保存方法执行了。。。");
// int a = 1/0; //制造异常通知
}
@Override
public void updateAccount(int id) {
System.out.println("更新方法执行了。。。");
}
@Override
public int deleteAccount() {
System.out.println("删除方法执行了。。。");
return 0;
}
}
<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
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="accountService" class="com.jk.xml.service.impl.AccountServiceImpl"/>
<bean id="aopUtil" class="com.jk.xml.utils.AopUtil"/>
<aop:config>
<aop:aspect id="aopAdvice" ref="aopUtil">
<aop:before method="beforeAdvice" pointcut="execution(int com.jk.xml.service.impl.*.*(..))"/>
aop:aspect>
aop:config>
beans>
aop
名称空间。@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:xml/bean.xml"})
public class TestBean1 {
@Autowired
AccountService as;
//测试通知配置
@Test
public void testBeforeAdvice(){
as.saveAccount();
as.updateAccount(1);
as.deleteAccount();
}
}
这里使用 Spring 整合了 Junit 测试单元,关于配置可以看这篇博客:Spring 整合 JUnit 测试单元
执行结果:
保存方法执行了。。。
更新方法执行了。。。
<==before,前置通知加上了==>
删除方法执行了。。。
int deleteAccount()
被增强了。以上就是关于 Spring AOP 的概念和动态代理模式的介绍,下一篇将会详细介绍切入点表达式、基于 xml 配置实现 AOP,基于注解实现 AOP,基于完全注解的方式实现 AOP,博客连接:
Spring AOP 的配置及使用