面向切面编程AOP

1.AOP是什么

​ AOP:Aspect-Oriented Programming(面向切面编程)

​ 在软件开发中,散布于应用中多处的功能被称为横切关注点(比如日志、安全和事务管理)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。

​ 简而言之,横切关注点可以被描述为影响应用多处的功能,AOP能帮助我们模块化横切关注点(切面)。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的业务逻辑类。

面向切面编程的好处:

  • 每个横切关注点都集中于一个地方,而不是分散到多处代码中。
  • 服务模块更简洁,因为它们只包含业务逻辑的代码,而横切关注点的代码被转移到一个统一的地方了。

2.AOP基本术语

2.1.连接点 ( Joinpoint )

​ 连接点是项目中可以被增强的代码段,从理论上讲,连接点可以是任意类型的代码段,但考虑到实现难度,通常会对连接点的代码段格式有更加严格的要求,例如,Spring AOP 仅支持方法级别的连接点。为简单起见,本书仅考虑以连接点为方法的情形,因此,AOP 可以看作是一种方法增强技术即采用横切关注点代码增强方法的功能。因此,在 Spring框架中,连接点可以简单地理解为方法。连接点可以被横切关注点置入,从而得到增强。

2.2.切入点( Pointcut )

​ 切点是真正需要插入切面的一个或多个连接点。即通知被应用的具体位置(在哪些连接点)。通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点(比如Aspect切点表达式)。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。

2.3.通知 (Advice )

​ 切面的工作被称为通知。即包含了需要用于多个应用对象的横切行为。

​ 通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?

Spring切面可以应用5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能。
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。
  • 返回通知(After-returning):在目标方法成功执行之后调用通知。
  • 异常通知(After-throwing)):在目标方法抛出异常后调用通知。
  • 环绕通知(Around) :通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

2.4.切面( Aspect )

所有类的总和

​ 切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。

2.5.织入 ( Weaving )

额外功能的加入

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。Aspect的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader) ,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving, LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时, AOP容器会为目标对象动态地创建一个代理对象(动态代理)。Spring AOP就是以这种方式织入切面的。

2.6.目标对象(Target Obiect )

​ 符合切人点描述的条件,被织人横切关注点的对象,称为目标对象。

2.7.代理 ( Proxy)

分为JDK代理和字节码代理

​ 代理是在横切关注点织人期间,动态创建的对象。代理是目标对象的增强对象。
AOP 基本术语之间的关系,可以用简短的一句话描述,即织入器按照切人点的描述将切面中定义的通知直接(如AspetJ)或间接(如Spring AOP)地织人到目录对象的连接点中。直接织人是在编译期间将通知编译到目标对象中,间接织人则是在运行时通过创建目标对象的代理实现通知织人的。

3.Aop实现机制

3.1静态代理

在 tools包中新建接口Axe,该接口含一个方法void chop()。

1
2
3
public interface Axe {
void chop();
}

在tools.impl包中新建Axe的实现类StealAxe。

1
2
3
4
5
public class SteelAxe implements Axe {
public void chop() {
System.out.println("用钢斧砍柴。");
}
}

在aspect包中,编写工具保养类ToolUpkeep(即切面),其包含两个增强方法(用于模拟增强功能)。

1
2
3
4
5
6
7
8
public class ToolUpkeep {
public void grind() {
System.out.println("工具已处于最佳工作状态。(后置)");
}
public void repair() {
System.out.println("工具已打磨维修好。(前置)");
}
}

创建proxy增强类包,AxeProxy继承Axe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AxeProxy implements Axe{
private ToolUpkeep toolUpkeep;
private Axe axe;
@Override
public void chop() {
if (toolUpkeep != null && axe != null) {
toolUpkeep.repair();
axe.chop();
toolUpkeep.grind();
} else {
System.out.println("工具未准备好,不能砍柴。");
}
}
}
//省略get、set方法

创建测试类方法

1
2
3
4
5
6
7
8
9
10
public class AxeTest {
@Test
public void AxeTest01() {
AxeProxy axeproxy = new AxeProxy();
axeproxy.setAxe(new StealAxe());
axeproxy.setToolUpkeep(new ToolUpkeep());
Axe axe = axeproxy;
axeproxy.chop();
}
}

代码效果:

1
2
3
工具已打磨维修好。(前置)
用钢斧砍柴。
工具已处于最佳工作状态。(后置)

3.2动态代理技术

在以上代码的基础上完成以下

在proxy包中,编写JDK静态代理类AxeProxyJdk,完成对*void* chop()方法的增强,即执行前的磨刀及执行后的维护功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AxeProxyJdk implements InvocationHandler{
private Axe axe;
public Object createDynamicProxy(Axe axe) {
this.axe=axe;
ClassLoader classLoader = AxeProxyJdk.class.getClassLoader();
Class[] classes = axe.getClass().getInterfaces();
return Proxy.newProxyInstance(classLoader, classes, this);
}
@Override
public Object invoke(Object arg0, Method arg1, Object[] arg2) throws Throwable {
// TODO Auto-generated method stub
ToolUpkeep toolupkeep = new ToolUpkeep();
toolupkeep.repair();
Object object = arg1.invoke(axe, arg2);
toolupkeep.grind();
return object;
}
}

在test/java目录中,创建测试类AxeTest,添加proxyJdkTest测试方法,测试利用JDK代理增加方法的效果。

1
2
3
4
5
6
7
8
public class AxeTest {
@Test
public void AxeTest01() {
AxeProxyJdk axeProxyJdk = new AxeProxyJdk();
Axe axe = (Axe) axeProxyJdk.createDynamicProxy(new StealAxe());
axe.chop();
}
}

输出结果

1
2
3
工具已维修好。
用钢斧砍柴。
工具已处于最佳工作状态。

3.3动态字节码生成技术代理

接第上个代码,在proxy 包中,编写CGLIB代理类AxeProxyCglib,完成对*void* chop()方法的增强,即执行前的磨刀及执行后的维护功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AxeProxyCglib implements MethodInterceptor {
public Object createAxeProxyCglib(Object target) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
ToolUpkeep toolUpkeep = new ToolUpkeep();
toolUpkeep.repair();
Object result = proxy.invokeSuper(obj, args);
toolUpkeep.grind();
return result;
}
}

在上述的测试类中添加以下测试方法

1
2
3
4
5
6
@Test
public void AxeTest02() {
AxeProxyCglib axeproxycglib = new AxeProxyCglib();
Axe axe = (Axe) axeproxycglib.createAxeProxyCglib(new StealAxe());
axe.chop();
}

输出结果同上

3.4基于XML的AOP实现(AspectJ)

在以上的代码基础上完成以下新建一个Maven项目

元素名 描述
aop:config/ Spring AOP配置的根元素。
aop:pointcut/ aop:config/aop:aspect/的子元素,用于配置切入点。
aop:advisor/ aop:config/的子元素,用于配置通知器。
aop:aspect/ aop:config/的子元素,用于配置切面。
aop:before/ aop:aspect/的子元素,用于配置前置通知。
aop:after/ aop:aspect/的子元素,用于配置最终通知。
aop:around/ aop:aspect/的子元素,用于配置环绕通知。
aop:after-returning/ aop:aspect/的子元素,用于配置后置通知。
aop:after-throwing/ aop:aspect/的子元素,用于配置异常通知。

aop:config/的子元素aop:pointcut/aop:advisor/aop:aspect/是顺序敏感的。

除了核心依赖及JUnit依赖外,添加以下依赖。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.9.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>

在aspectj包中,创建ToolUpkeep切面类,在类中依次添加前置通知、后置通知、环绕通知、异常通知及最终通知等5个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ToolUpkeep {
public void grind() {
System.out.println("工具已处于最佳工作状态。(后置)");
}
public void repair() {
System.out.println("工具已打磨维修好。(前置)");
}
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("使用前保养,将斧子磨锋利一些");
}
public void afterReturningAdivce(JoinPoint joinPoint) {
System.out.println("收拾柴火回家");
}
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("去砍柴地点");
Object object = proceedingJoinPoint.proceed();
System.out.println("收斧,休息一下");
return proceedingJoinPoint;
}
private void afterAdvice() {
System.out.println("砍柴结束,期间未出现意外,记录下这次事件");
}
public void exceptionAdvice(Exception e) {
System.out.println("出现异常:"+e);
}
}

在main/resources目录下,创建文件夹config,并在其中创建beans.xml文件

注意其中bean对象和aop对象中包名映射的路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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-4.3.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="axe" class="com.example.java_test06.tools.impl.SteelAxe" />
<bean id="toolUpkeep" class="com.example.java_test06.aspect.ToolUpkeep" />
<aop:config proxy-target-class="true">
<aop:pointcut id="pointCut" expression="execution(* com.example.java_test06.tools.impl.SteelAxe.*(..))"/>
<aop:aspect ref="toolUpkeep">
<aop:before method="beforeAdvice" pointcut-ref="pointCut"/>
<aop:around method="aroundAdvice" pointcut-ref="pointCut"/>
<aop:after-returning method="afterReturningAdvice" pointcut-ref="pointCut"/>
<aop:after-throwing method="exceptionAdvice" throwing="e" pointcut-ref="pointCut"/>
<aop:after method="afterAdvice" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

注意:以下标红部分

<aop:pointcut id=”pointCut“ expression=”execution(* com.example.java_test06.tools.impl.SteelAxe.*>(..))”/>

<aop:before method=”beforeAdvice” pointcut-ref=”pointCut“/>

其中pointcut-ref后面的属性对应其上中的id中的属性,表示向其中id名为“pointCut”的切入点做切入操作

第一个标红的*表示筛选其中所有方法名的返回值:

例如替换成int则说明其中只会筛选返回值为int的方法做切入

第二个标红的*表示筛选类中的所有的子包,括号中的..表示其中所有包含它可以有零个、一个或多个参数,都会被添加切面,如果使用 * 则会只增加有一个或多个参数的函数而不扫面没有函数的。

创建AxeTest测试类,编写测试方法chopTest方法

1
2
3
4
5
6
7
8
public class AxeTest {
@Test
public void chopTest(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("config/beans.xml");
SteelAxe axe = applicationContext.getBean(SteelAxe.class);
axe.chop();
}
}

输出结果

1
2
3
4
5
6
使用前保养,将斧子磨锋利一些
去砍柴地点
用钢斧砍柴。
收斧,休息一下
收拾柴火回家
砍柴结束,期间未出现意外,记录下这次事件

3.5基于注解声明的AOP实现

注解 描述
@Aspect 配置切面。
@Pointcut 配置切入点。
@Before 配置前置通知。
@After 配置最终通知。
@Around 配置环绕通知。
@AfterReturning 配置后置通知。
@AfterThrowing 配置异常通知。

Spring AOP支持注解方式的实现,采用注解可减少配置文件中的配置信息,降低维护成本。在3.4实验的基础上修改beans.xml文件中的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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-4.3.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.example.java_test06.tools.impl,com.example.java_test06.aspect"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

****说明****:beans.xml文件中,context:component-scan/元素的属性base-package值也可以改为”com.example.java_test06”,即两个包路径的公共包路径。

根据beans.xml文件中配置信息的变化,在ToolUpkeep类中添加注解信息,但需要保持切面、切入点配置不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Component
@Aspect
public class ToolUpkeep {
@Pointcut("execution(* com.example.java_test06.tools.impl.SteelAxe.*(..))")
public void pointCut(){}
@Before("pointCut()")
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("使用前保养,将斧子磨锋利一些");
}
@AfterReturning("pointCut()")
public void afterReturningAdvice(JoinPoint joinPoint){
System.out.println("收拾柴火回家");
}
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
System.out.println("去砍柴地点");
Object object = proceedingJoinPoint.proceed();
System.out.println("收斧,休息一下");
return object;
}
@AfterThrowing(value = "pointCut()",throwing = "e")
public void exceptionAdvice(Exception e){
System.out.println("异常通知,异常信息是:" + e.toString());
}
@After("pointCut()")
public void afterAdvice(){
System.out.println("砍柴结束,期间未出现意外,记录下这次事件");
}
}

测试类同上,测试结果同上

4.小结

AOP是Spring的核心功能之一,本章在给出AOP基本术语的基础上,详细介绍了AOP的实现机制,即动态代理和动态字节码生成技术。AspectJ是Java平台中对AOP支持最完善的产品之一,其拥有简易的切面定义和灵活的切入点描述等优秀特性,支持基于XML和注解两种实现方式,深受广大开发人员的喜爱。