Spring AOP原理
概念
AOP(即Aspect Oriented Program)面向切面编程
“横切”的技术,剖解开封装的对象内部,将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为“Aspect”,即切面。所谓“切面”,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任(如日志管理、事务处理、权限认证等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,有利于未来的可操作性和可维护性。
使用“横切”技术,AOP把软件系统分为两个部分:
- 核心关注点:业务处理的主要流程,比如登录、增加数据、删除数据等。
- 横切关注点:与核心关注点无关的部分,如权限认证、日志、事务等。(特点:经常发生在核心关注点的多处,而各处基本相似。)
AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
主要应用场景
- Authentication 权限
- Caching 缓存
- Context passing 内容传递
- Error handling 错误处理
- Lazy loading 懒加载
- Debugging 调试
- logging,tracing,profiling and monitoring 记录、跟踪、优化和校准
- Performance optimization 性能优化
- Persistence 持久化
- Resource pooling 资源池
- Synchronization 同步
- Transactions 事务
AOP核心概念
- 切面(aspect):类是物体特征的抽象,切面就是横切关注点的抽象。一个关注点的模块化,这个关注点可能横切多个对象,例如事务管理。在Spring AOP中,切面可以使用基于模式或者基于@Aspect注解的方式来实现。切面=切入点+通知,通俗点就是:在什么时机,什么地方,做什么增强!
- 连接点(joinpoint):在程序执行过程中某个特定的点。在Spring AOP中,一个连接点总是标识一个方法的执行。实际上连接点还可以是字段或者构造器。
- 切入点(pointcut):在哪些类、哪些方法上切入(where)。匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如 当执行某个特定名称的方法时)。切入点表达式如何跟连接点匹配是AOP的核心。
- 通知(advice):所谓通知就是指拦截到连接点之后,要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类。许多AOP框架(包括Spring)都是以拦截器作为通知模型,并维护一个以连接点为中心的拦截器链。在方法执行的什么时机(when:方法前/方法后/方法前后),做什么(what:增强功能)。
- 引入(introduction):用来给一个类型声明额外的方法或属性(也被成为连接类型声明(inner-tyoe declaration))。Srping允许引入新的接口()以及一个对应的实现)到任何被代理的对象。例如,可以使用引入来使一个bean实现IsModified接口,以便建华缓存机制。
- 织入(weaving):把切面加入到对象,并创建出代理对象的过程(由Spring来完成),这些可以在编译时、类加载时和运行时完成。
- AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
- 目标对象(target object):被一个或多个切面所通知的对象,也称作被通知(advised)对象。既然Sprign AOP是通过运行时代理实现的,则这个对象永远是一个被代理(proxied)对象。
AOP的两种代理方式
JDK动态接口代理
主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。
- InvocationHandler是一个接口,通过实现该接口,定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。
- Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。
CGLib动态代理
CGLib全称Code Generation Library,是一个强大的高性能、高质量的代码生成类库,可在运行期扩展Java类和实现Java接口,还封装了 asm,可在运行期动态生成新的class。
为了更好的说明AOP的概念,举一个实际中的例子来说明:
在上面的业务中,包租婆的核心业务就是签合同、收租金,而其他部分就是重复且边缘的事,交给中介就好了。这就是AOP的一个思想:让关注点代码与业务代码分离。
实际代码
1、在 Package【pojo】创建一个Landlord类
package pojo;
import org.springframework.stereotype.Component;
@Component("landlord")
public class Landlord {
public void service() {
// 仅仅只是实现了核心的业务功能
System.out.println("签合同");
System.out.println("收房租");
}
}
2、在 Package【aspect】下创建中介Broker类
package aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
class Broker {
@Before("execution(* pojo.Landlord.service())")
public void before(){
System.out.println("带租客看房");
System.out.println("谈价格");
}
@After("execution(* pojo.Landlord.service())")
public void after(){
System.out.println("交钥匙");
}
}
3、在 applicationContext.xml 中配置自动注入,并告诉 Spring IoC 容器去哪里扫描这两个 Bean:
<?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"
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 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="aspect" />
<context:component-scan base-package="pojo" />
<aop:aspectj-autoproxy/>
</beans>
4、在 Package【test】下编写测试代码:
package test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pojo.Landlord;
public class TestSpring {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
Landlord landlord = (Landlord) context.getBean("landlord", Landlord.class);
landlord.service();
}
}
5、执行结果:
我们在 Landlord 的 service() 方法中仅仅实现了核心的业务代码,其余的关注点功能是根据我们设置的切面自动补全的。
使用注解来开发Spring AOP
第一步:选择连接点
Spring是方法级别的AOP框架,我们也是主要以某个类的某个方法作为连接点,另一种说法就是:选择哪一个类的哪一个方法用以增强功能。
这里选择Landlord类中的service()方法作为连接点。
....
public void service() {
// 仅仅只是实现了核心的业务功能
System.out.println("签合同");
System.out.println("收房租");
}
....
第二步:创建切面
选择好连接点就可以创建切面了,我们可以把切面理解为一个拦截器,当程序运行到连接点的时候,被拦截下来,在开头加入了初始化的方法,在结尾也加入了销毁的方法而已。在Srping中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面了:
package aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
class Broker {
@Before("execution(* pojo.Landlord.service())")
public void before(){
System.out.println("带租客看房");
System.out.println("谈价格");
}
@After("execution(* pojo.Landlord.service())")
public void after(){
System.out.println("交钥匙");
}
}
注意: 被定义为切面的类仍然是一个 Bean ,需要 @Component 注解标注
代码部分中在方法上面的注解看名字也能猜出个大概,下面来列举一下 Spring 中的 AspectJ 注解:
注解 | 说明 |
---|---|
@Before | 前置通知,在连接点方法前调用 |
@Around | 环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法,后面会讲 |
@After | 后置通知,在连接点方法后调用 |
@AfterReturning | 返回通知,在连接点方法执行并正常返回后调用,要求连接点方法在执行过程中没有发生异常 |
@AfterThrowing | 异常通知,当连接点方法异常时调用 |
有了上表,我们就知道 before() 方法是连接点方法调用前调用的方法,而 after() 方法则相反,这些注解中间使用了定义切点的正则式,也就是告诉 Spring AOP 需要拦截什么对象的什么方法,下面讲到。
第三步:定义切点
在上面的注解中定义了 execution 的正则表达式,Spring 通过这个正则表达式判断具体要拦截的是哪一个类的哪一个方法:
execution(* pojo.Landlord.service())
依次对这个表达式作出分析:
- execution:代表执行方法的时候触发
- *:代表任意返回类型的方法
- pojo.Landlord:代表类的全限定名
- service():代表被拦截的方法名称
通过上面的表达式,Spring就会知道应该拦截pojo.Landlord类下的service()方法。上面的演示类还好,如果多处都需要写这样的表达式难免会有些复杂,我们可以通过使用 @Pointcut 注解来定义一个切点来避免这样的麻烦:
package aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
class Broker {
@Pointcut("execution(* pojo.Landlord.service())")
public void lService() {
}
@Before("lService()")
public void before() {
System.out.println("带租客看房");
System.out.println("谈价格");
}
@After("lService()")
public void after() {
System.out.println("交钥匙");
}
}
第四步:测试 AOP
环绕通知
环绕通知,是Spring AOP最强大的通知,因为它集成了前置通知和后置通知,它保留了连接点原有方法的功能,所以它强大又灵活。
package aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect
class Broker {
// 注释掉之前的 @Before 和 @After 注解以及对应的方法
// @Before("execution(* pojo.Landlord.service())")
// public void before() {
// System.out.println("带租客看房");
// System.out.println("谈价格");
// }
//
// @After("execution(* pojo.Landlord.service())")
// public void after() {
// System.out.println("交钥匙");
// }
// 使用 @Around 注解来同时完成前置和后置通知
@Around("execution(* pojo.Landlord.service())")
public void around(ProceedingJoinPoint joinPoint) {
System.out.println("带租客看房");
System.out.println("谈价格");
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("交钥匙");
}
}
使用XML配置开发Spring AOP
注解是很强大的东西,但基于 XML 的开发我们仍然需要了解,我们先来了解一下 AOP 中可以配置的元素:
AOP 配置元素 | 用途 | 备注 |
---|---|---|
aop:advisor | 定义 AOP 的通知 | 一种很古老的方式,很少使用 |
aop:aspect | 定义一个切面 | —— |
aop:before | 定义前置通知 | —— |
aop:after | 定义后置通知 | —— |
aop:around | 定义环绕通知 | —— |
aop:after-returning | 定义返回通知 | —— |
aop:after-throwing | 定义异常通知 | —— |
aop:config | 顶层的 AOP 配置元素 | AOP 的配置是以它为开始的 |
aop:declare-parents | 给通知引入新的额外接口,增强功能 | —— |
aop:pointcut | 定义切点 | —— |
有了之前通过注解来编写的经验,并且有了上面的表,我们将上面的例子改写成 XML 配置很容易(去掉所有的注解):
<!-- 装配 Bean-->
<bean name="landlord" class="pojo.Landlord"/>
<bean id="broker" class="aspect.Broker"/>
<!-- 配置AOP -->
<aop:config>
<!-- where:在哪些地方(包.类.方法)做增加 -->
<aop:pointcut id="landlordPoint"
expression="execution(* pojo.Landlord.service())"/>
<!-- what:做什么增强 -->
<aop:aspect id="logAspect" ref="broker">
<!-- when:在什么时机(方法前/后/前后) -->
<aop:around pointcut-ref="landlordPoint" method="around"/>
</aop:aspect>
</aop:config>
参考资料:
简书ID:@我没有三颗心脏