简书 Wwwwei
转载请注明原创出处,谢谢!
前言
上篇文章中,我们介绍了有关Spring IoC控制反转的概念以及实现,接下来我们将说一说Spring的另一重大特点AOP面向切面编程。同样,还是老套路,我们将从是什么、怎么做、为什么三个主要方向入手,说清楚AOP到底是个什么东西、Spring 怎么做实现了AOP以及为什么要使用AOP这几个问题。
什么是AOP?
官方解释
我们先来看一下比较官方的解释。
AOP,Aspect Oriented Programming的缩写,意为面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
依然没看懂ㄟ( ▔, ▔ )ㄏ。
生活中的AOP
要理解AOP(面向切面编程),首先我们要明白什么叫做切面。
举个生活中的例子,我们都知道一个成功的老板背后一定有一个优秀的秘书,而秘书的职责就是负责调度和安排老板的日程。想象一下这样一个场景,今天秘书小M将事先安排好的一天日程向老板大B汇报后,老板大B表示同意。那么如无意外,老板大B今天将和往常一样,按照日程表一样一样完成,一天就过去了。但是,秘书小M突然接到通知,一个十分重要的会议临时决定在午后召开,无奈秘书小M只能将该会议强行插入老板大B已经确认过的日程安排中,然后默默等待老板大B的白眼。
我们注意到秘书小M将临时会议插入日程的动作,是不是很符合切面这个感觉呢?更形象一点,我们将老板大B确认过的日程想象成一条长面包,而秘书小M突然被告知还有一片火腿(即那个讨厌的临时会议),为了老板能够吃好,不得已只能用小刀将面包切开后硬生生将火腿塞入。
可以这样理解,面向切面就是为了达到目的,动态地将某件事切入到某件事中。
程序中的AOP
回到代码层面,关于AOP我在网上博文中发现了这样一句话:运行时,动态地将代码切入到类的指定方法或者指定位置。是不是豁然开朗了呢?
这里还想强调一下运行时和动态两个点,运行时不难理解,如果在程序非运行时我们只要将代码写入指定位置再运行就好了,而程序运行时,代码就无法进行更改了,这也体现了不修改源代码的意义;动态这个概念我们可以这样理解,切入的过程并非是事先完成好的,而是在程序运行过程中触发了某个时机而进行的。
此处介绍几个概念,便于读者理解:
通知(advice):切入到类指定方法或者指定位置的代码片段,即需要增加的功能代码,也就是上述的那片火腿(临时会议)。
连接点(join point):程序运行过程中能够进行插入切面操作的时间点。例如方法调用、异常抛出或字段修改等,可以理解为上述的长面包(老板的整个日程安排)。
切入点(pointcut):描述一个通知将被切入的一系列连接点的集合,即代码片段具体切入到哪些类、哪些方法,也就是上述面包切口处(特指午后的日程)。所以说,切入点规定了哪些连接点可以执行哪些通知。
切面(aspect):AOP中的切面等同于OOP中的类(class),由通知(advice)和切入点(pointcut)组成,其中通知(advice)和切入点(pointcut)既可以是1对1的关系,也可以是1对多的关系。概括的说就是描述了何时何地干何事的基本单元,其中通知(advice)说明了切面干何事,而切入点则说明了切面何时何地切入。
关于几者的关系,我们可以这样理解,通知是在连接点上执行的,但是我们不希望通知应用到所有的连接点,所以引入了切入点来匹配特定的连接点,指名我们所希望通知应用的连接点。
因此,所谓的AOP(面向切面编程)就是在程序运行过程中的某个时机将代码片段插入到某些类的指定方法和指定位置;换句话说,秘书接到临时会议通知时,将临时会议插入到老板的日程安排中去。
为什么要使用AOP?
程序的最终目的就是实现业务,但是我们在进行编程的过程中经常会发现除了所谓的业务代码,还存在数量相当的公共代码,类似日志、安全验证、事物、异常处理等问题。这部分代码重要但是与我们编写程序要实现的功能没有关系,具有功能相似、重用性高、使用场景分散等特点。我们姑且称它们为共性问题。
对大多数程序而言,代码都是以纵向结构将各个业务模块串联从而完成功能的。我们提到的共性问题本身不属于业务范围,但是又散落在各个业务模块间,同实现主功能的代码相互杂糅在一起,即如下图所示:
试想一下,如果将共性问题部分的代码融入业务代码中,一旦涉及到对某个共性问题部分的代码进行更改的时候,例如日志部分发生需求变更,我们可能需要牵涉许许多多其他模块代码。这在小规模程序中也许是可以接受的,可能只修改1、2处;但是如果牵涉的地方数量过多,特别是应用在中大型规模程序中,我们甚至会为了小小的一个功能,修改上千、上万处。这样的方式是十分糟糕的,不仅费时费力,可能还会引起一些不必要的麻烦(回归错误、结构混乱等等)。
AOP的就是为了解决这类共性问题,将散落在程序中的公共部分提取出来,以切面的形式切入业务逻辑中,使程序员只专注于业务的开发,从事务提交等与业务无关的问题中解脱出来。
AOP的好处
解耦:AOP将程序中的共性问题进行了剥离,毫无疑问地降低了各个业务模块和共性问题之间的耦合。
重用性:共性问题散落于业务逻辑的各处,十分难维护,使用AOP进行提取后,能够将相似功能的共性问题收敛,减少重复代码,提高了代码的重用性。
拓展性:对于一个程序而言,迭代的重心一定在于业务和功能上。AOP使得每当发生变更时,可以只关注业务逻辑相关的代码,而减少共性问题上带来的变化,大大降低了程序未来拓展的成本。
Spring如何实现AOP?
什么是Spring AOP?
通过上述介绍,我们了解到AOP的重心在于定义基本结构和完成切面切入两部分,解决了它们,我们就能方便快捷的使用AOP思想进行编程了。
Spring AOP就是负责完成AOP相关工作的框架,它将切面所定义的横切逻辑切入到切面所指定的连接点中。框架的目的就是将复杂的事情变得简单易用,所以其主要工作分成了如下两点:
1.提供相应的数据结构来定义AOP所需基本结构,例如通知、连接点、切入点、切面等。
2.封装切面切入的相关工作,提供相应接口给用户。封装的内容主要包括如何通过切面(切入点和通知)定位到特定的连接点;如何将切面中的功能代码植入到特定的连接点中等等;提供相应接口主要包括配置文件、注解等用户能够使用的工具。
这里想提一下,在 Spring AOP 中,连接点(join point)总是方法的执行点, 即只有方法连接点。所以我们可以认为,在 Spring 中所有的方法都可以是连接点。
怎么使用Spring AOP?
由于Spring对AOP的封装,使得我们可以十分方便的使用,我们只需要定义切面,即定义Advice通知和Pointcut切入点。
(1)Spring AOP中Pointcut切入点
使用@Pointcut("execution()")注解定义切入点,其中execution的格式如下所示:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
其中,除ret-type-pattern和name-pattern之外,其他都是可选的,各部分具体含义如下:
1.modifiers-pattern:方法的操作权限
2.ret-type-pattern:返回值
3.declaring-type-pattern:方法所在的包
4.name-pattern:方法名
5.parm-pattern:参数名
6.throws-pattern:异常
为了更加理解,举一个简单的execution示例,如下所示:
(2)Spring AOP中Advice通知
- before advice:前置通知,由@Before注解定义,表示在 join point 前被执行的advice。(虽然before advice是在 join point 前被执行,但是它并不能够阻止 join point 的执行, 除非发生了异常,即在before advice代码中, 不能人为地决定是否继续执行join point中的代码)
- after return advice:后置通知,由@AfterReturning注解定义,表示在一个 join point 正常返回后执行的advice。
- after throwing advice:异常通知,由@AfterThrowing注解定义,表示当一个 join point 抛出异常后执行的advice。
- after(final) advice:最终通知,由@After注解定义,表示无论一个join point是正常退出还是发生了异常,都会被执行的advice。
- around advice:环绕通知,由@Around注解定义,表示在join point 前和joint point退出后都执行的 advice,是最常用的advice。
(3)举个例子
首先,我们创建一个简单的UserService,作为示例的业务模块,代码如下:
package com.demo.aop;
import org.springframework.stereotype.Service;
/**
* Created by wwwwei on 17/8/14.
*/
@Service
public class UserService {
//创建用户
public void createUser(String userName) {
System.out.println("创建用户 " + userName);
}
//删除用户
public void deleteUser(String userName) {
System.out.println("删除用户 " + userName);
}
//更新用户
public void updateUser(String userName) {
System.out.println("更新用户 " + userName);
throw new RuntimeException("更新用户抛出异常");
}
}
其次,定义切面数据结构,即通知(需要增加的功能代码片段)和切入点(切入的时间点),代码如下:
package com.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* Created by wwwwei on 17/8/11.
*/
@Component
@Aspect
public class UserAspect {
//定义切入点
@Pointcut("execution(* com.demo.aop.*Service*.*(..))")
public void pointCut() {
}
//前置通知 @Before(value="execution(public * *(..))")
@Before("pointCut()")
public void mybefore() {
System.out.println("前置通知");
}
//后置通知 @AfterReturning(value="execution(public * *(..))")
@AfterReturning(pointcut = "pointCut()")
public void myafterReturning() {
System.out.println("后置通知");
}
//异常通知 @AfterThrowing(value="execution(public * *(..))")
@AfterThrowing(pointcut = "pointCut()", throwing = "error")
public void myafterThrowing() {
System.out.println("异常通知");
}
//环绕通知 @Around(value="execution(public * *(..))")
@Around("pointCut()")
public void myAround(ProceedingJoinPoint jp) {
System.out.println("环绕前通知");
try {
jp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("环绕后通知");
}
//最终通知 @After(value="execution(public * *(..))")
@After("pointCut()")
public void myafterLogger() {
System.out.println("最终通知");
}
}
最后,编写测试类测试,代码如下:
package com.demo.aop;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* Created by wwwwei on 17/8/14.
*/
public class UserTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(
"applicationContext.xml");
UserService userService = context.getBean(UserService.class);
userService.createUser("测试用户");
}
}
运行结果如下,可以发现在业务模块中成功切入了我们定义的代码片段。
AOP的实现
AOP的实现是基于代理机制的,根据不同实现方式主要分为两类:
1.静态代理,AOP框架会在编译阶段生成AOP代理类,即在编译器和类装载期实现切入的工作,但是这种方式需要特殊的Java编译器和类装载器。AspectJ框架就是采用这种方式实现AOP。
2.动态代理,AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的连接点(切入点)做了添加通知(advice)处理,并回调原对象的方法。
与AspectJ的静态代理不同,Spring AOP使用动态代理,通过JDK Proxy和CGLIB Proxy两种方法实现代理。两种方式的选择与目标对象有关:
- 如果目标对象没有实现任何接口,那么Spring将使用CGLIB来实现代理。CGLIB是一个开源项目,它是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。
- 如果目标对象实现了一个以上的接口,那么Spring将使用JDK Proxy来实现代理,因为Spring默认使用的就是JDK Proxy,并且JDK Proxy是基于接口的。这也是Spring提倡的面向接口编程。当然,你也可以强制使用CGLIB来进行代理,但是这样可能会造成性能上的下降。
感谢以下博文,写作时作为参考借鉴。
Spring AOP的实现原理
彻底征服 Spring AOP 之 理论篇