一、AOP介绍
AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等。AOP 实现的关键就在于 AOP 框架自动创建的 AOP 代理,AOP 代理则可分为静态代理和动态代理两大类,其中静态代理是指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于 JDK 动态代理、CGLIB 等在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。
二、AOP存在的价值
在传统 OOP 编程里以对象为核心,整个软件系统由系列相互依赖的对象所组成,而这些对象将被抽象成一个一个的类,并允许使用类继承来管理类与类之间一般到特殊的关系。随着软件规模的增大,应用的逐渐升级,慢慢出现了一些 OOP 很难解决的问题。
我们可以通过分析、抽象出一系列具有一定属性与行为的对象,并通过这些对象之间的协作来形成一个完整的软件功能。由于对象可以继承,因此我们可以把具有相同功能或相同特性的属性抽象到一个层次分明的类结构体系中。随着软件规范的不断扩大,专业化分工越来越系列,以及 OOP 应用实践的不断增多,随之也暴露出了一些 OOP 无法很好解决的问题。
现在假设系统中有 3 段完全相似的代码,这些代码通常会采用“复制”、“粘贴”方式来完成,通过这种“复制”、“粘贴”方式开发出来的软件如图 1 所示。
看到如图 1 所示的示意图,可能有的读者已经发现了这种做法的不足之处:如果有一天,图 1 中的深色代码段需要修改,那是不是要打开 3 个地方的代码进行修改?如果不是 3 个地方包含这段代码,而是 100 个地方,甚至是 1000 个地方包含这段代码段,那会是什么后果?
为了解决这个问题,我们通常会采用将如图 1 所示的深色代码部分定义成一个方法,然后在 3 个代码段中分别调用该方法即可。在这种方式下,软件系统的结构如图 2 所示。
对于如图 2 所示的软件系统,如果需要修改深色部分的代码,只要修改一个地方即可,不管整个系统中有多少地方调用了该方法,程序无须修改这些地方,只需修改被调用的方法即可——通过这种方式,大大降低了软件后期维护的复杂度。
对于如图 2 所示的方法 1、方法 2、方法 3 依然需要显式调用深色方法,这样做能够解决大部分应用场景。但对于一些更特殊的情况:应用需要方法 1、方法 2、方法 3 彻底与深色方法分离——方法 1、方法 2、方法 3 无须直接调用深色方法,那如何解决?
这样做的工作量也不小啊!我们希望有一种特殊的方法:我们只要定义该方法,无须在方法 1、方法 2、方法 3 中显式调用它,系统会“自动”执行该特殊方法。
上面想法听起来很神奇,甚至有一些不切实际,但其实是完全可以实现的,实现这个需求的技术就是 AOP。AOP 专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题,在 Java EE 应用中,常常通过 AOP 来处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等,AOP 已经成为一种非常常用的解决方案。
三、AOP的相关术语
术语 | 中文 | 描述 |
---|---|---|
Joinpoint | 连接点 | 指那些被拦截到的点.在Spring中,这些点指方法(因为Spring只支持方法类型的连接点). |
Pointcut | 切入点 | 指需要(配置)被增强的Joinpoint. |
Advice | 通知/增强 | 指拦截到Joinpoint后要做的操作.通知分为前置通知/后置通知/异常通知/最终通知/环绕通知等. |
Aspect | 切面 | 切入点和通知的结合. |
Target | 目标对象 | 需要被代理(增强)的对象. |
Proxy | 代理对象 | 目标对象被AOP 织入 增强/通知后,产生的对象. |
Weaving | 织入 | 指把增强/通知应用到目标对象来创建代理对象的过程 (Spring采用动态代理织入,AspectJ采用编译期织入和类装载期织入). |
Introduction | 引介 | 一种特殊通知,在不修改类代码的前提下,可以在运行期为类动态地添加一些Method/Field(不常用). |
四、Spring AOP实现
Spring AOP代理实现方式有两种:JDK动态代理和Cglib框架动态代理,具体的实现可参考:Java动态代理
Spring AOP的底层通过JDK/Cglib动态代理为目标对象进行横向织入;
1)若目标对象实现了接口,则Spring使用JDK的 java.lang.reflect.Proxy代理
2) 若目标没有实现接口,则Spring使用cglib库生成目标对象的子类
- Spring只支持方法连接点,不提供属性连接.
- 标记为
final
的方法不能被代理,因为无法进行覆盖. - 程序应优先对针对接口代理,这样便于程序解耦/维护.
五、Spring AOP
AOP联盟为通知Advice
定义了org.aopalliance.aop.Advice
接口, Spring在Advice
的基础上,根据通知在目标方法的连接点位置,扩充为以下五类:
通知 | 接口 | 描述 |
---|---|---|
前置通知 | MethodBeforeAdvice |
在目标方法执行前实施增强 |
后置通知 | AfterReturningAdvice |
…执行后实施增强 |
环绕通知 | MethodInterceptor |
..执行前后实施增强 |
异常抛出通知 | ThrowsAdvice |
…抛出异常后实施增强 |
引介通知 | IntroductionInterceptor |
在目标类中添加新的方法和属性(少用) |
- 添加Spring的AOP依赖
使用Spring的AOP和AspectJ需要在pom.xml中添加如下依赖:
1
2
3
4
5
6
7
8
9
10
|
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> |
- 定义Target
1
2
3
4
5
6
7
8
9
10
|
/** * @author jifang * @since 16/3/3 下午2:50. */ public interface OrderService { void save(); Integer delete(Integer param); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class OrderServiceImpl implements OrderService { @Override public void save() { System.out.println( "添加..." ); } @Override public Integer delete(Integer param) { System.out.println( "删除..." ); return param; } } |
- 定义Advice
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/** * 实现MethodInterceptor接口定义环绕通知 * * @author jifang * @since 16/3/6 下午2:54. */ public class ConcreteInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println( "前置通知 -> " ); Object result = invocation.proceed(); System.out.println( "<- 后置通知" ); return result; } } |
Spring手动代理
- 配置代理
Spring最原始的AOP支持, 手动指定目标对象与通知(没有使用AOP名称空间).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<?xml version= "1.0" encoding= "UTF-8" ?> <beans xmlns= "http://www.springframework.org/schema/beans" xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http: //www.springframework.org/schema/beans http: //www.springframework.org/schema/beans/spring-beans.xsd"> <!-- target --> <bean id= "service" class = "com.fq.service.impl.OrderServiceImpl" /> <!-- advice --> <bean id= "advice" class = "com.fq.advice.ConcreteInterceptor" /> <bean id= "serviceProxy" class = "org.springframework.aop.framework.ProxyFactoryBean" > <property name= "target" ref= "service" /> <property name= "interceptorNames" value= "advice" /> <property name= "proxyTargetClass" value= "false" /> </bean> </beans> |
- Client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (locations = "classpath:spring/applicationContext.xml" ) public class AOPClient { @Autowired // 必须指定使用代理对象名称, 否则不予代理 @Qualifier ( "serviceProxy" ) private OrderService service; @Test public void client() { service.save(); service.delete( 88 ); } } |
这种方式的缺陷在于每个
Target
都必须手动指定ProxyFactoryBean
对其代理(不能批量指定),而且这种方式会在Spring容器中存在两份Target对象(代理前/代理后),浪费资源,且容易出错(比如没有指定@Qualifier
).
Spring自动代理 – 引入AspectJ
通过AspectJ引入Pointcut切点定义
- Target/Advice同前
- 定义切面表达式
通过execution函数定义切点表达式(定义切点的方法切入)
execution(<访问修饰符> <返回类型><方法名>(<参数>)<异常>)
如:
1)execution(public * *(..))
# 匹配所有public
方法.
2)execution(* com.fq.dao.*(..))
# 匹配指定包下所有类方法(不包含子包)
3)execution(* com.fq.dao..*(..))
# 匹配指定包下所有类方法(包含子包)
4)execution(* com.fq.service.impl.OrderServiceImple.*(..))
# 匹配指定类所有方法
5)execution(* com.fq.service.OrderService+.*(..))
# 匹配实现特定接口所有类方法
6)execution(* save*(..))
# 匹配所有save开头的方法
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
31
|
<?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: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"> <!-- target --> <bean id= "service" class = "com.fq.service.impl.OrderServiceImpl" /> <!-- advice --> <bean id= "advice" class = "com.fq.advice.ConcreteInterceptor" /> <!-- 配置切面 : proxy-target- class 确定是否使用CGLIB --> <aop:config proxy-target- class = "true" > <!-- aop:pointcut : 切点定义 aop:advisor: 定义Spring传统AOP的切面,只支持一个pointcut/一个advice aop:aspect : 定义AspectJ切面的,可以包含多个pointcut/多个advice --> <aop:pointcut id= "pointcut" expression= "execution(* com.fq.service.impl.OrderServiceImpl.*(..))" /> <aop:advisor advice-ref= "advice" pointcut-ref= "pointcut" /> </aop:config> </beans> |
- Client同前
AspectJ AOP
AspectJ是一个基于Java的AOP框架,提供了强大的AOP功能,其他很多AOP框架都借鉴或采纳了AspectJ的一些思想,Spring2.0以后增加了对AspectJ切点表达式支持(如上),并在Spring3.0之后与AspectJ进行了很好的集成.
在Java领域,AspectJ中的很多语法结构基本上已成为AOP领域的标准, 他定义了如下几类通知类型:
通知 | 接口 | 描述 |
---|---|---|
前置通知 | @Before |
相当于BeforeAdvice |
后置通知 | @AfterReturning |
相当于AfterReturningAdvice |
环绕通知 | @Around |
相当于MethodInterceptor |
抛出通知 | @AfterThrowing |
相当于ThrowAdvice |
引介通知 | @DeclareParents |
相当于IntroductionInterceptor |
最终final通知 | @After |
不管是否异常,该通知都会执行 |
新版本Spring,建议使用AspectJ方式开发以简化AOP配置.
AspectJ-XML-AOP
使用AspectJ编写Advice无需实现任何接口,而且可以将多个通知写入一个切面类.
前置通知
- 定义通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/** * @author jifang * @since 16/3/3 下午5:38. */ public class Aspect { /** * 无返回值 */ public void before1() { System.out.println( "前置增强before1" ); } /** * 还可以传入连接点参数 JoinPoint * * @param point */ public void before2(JoinPoint point) { System.out.printf( "前置增强before2 %s%n" , point.getKind()); } } |
- 装配
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
31
32
33
34
35
36
|
<?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:aop= "http://www.springframework.org/schema/aop" xmlns:context= "http://www.springframework.org/schema/context" 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 http: //www.springframework.org/schema/context http: //www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base- package = "com.fq.service" /> <!-- 配置切面通知 --> <bean id= "advice" class = "com.fq.advice.Aspect" /> <!-- AOP切面配置 --> <aop:config> <aop:aspect ref= "advice" > <aop:pointcut id= "pointcut" expression= "execution(* com.fq.service.impl.OrderServiceImpl.*(..))" /> <aop:before method= "before1" pointcut-ref= "pointcut" /> <aop:before method= "before2" pointcut-ref= "pointcut" /> </aop:aspect> </aop:config> </beans> |
- 前置通知小结
- 前置通知会保证在目标方法执行前执行;
- 前置通知默认不能阻止目标方法执行(但如果通知抛出异常,则目标方法无法执行);
- 可以通过
JoinPoint
参数获得当前拦截对象和方法等信息.
后置通知
- 定义通知
1
2
3
|
public void afterReturning(JoinPoint point, Object result) { System.out.printf( "后置增强, 结果为 %s%n" , result); } |
- 装配
1
|
<aop:after-returning method= "afterReturning" returning= "result" pointcut-ref= "pointcut" /> |
后置通知可以获得方法返回值,但在配置文件定义返回值参数名必须与后置通知方法参数名一致(如
result
).
环绕通知
- 定义通知
1
2
3
4
5
6
7
8
9
|
public Object around(ProceedingJoinPoint point) throws Throwable { System.out.printf( "环绕前置增强 method: %s, args: %s%n" , point.toShortString(), Arrays.toString(point.getArgs())); Object result = point.proceed(point.getArgs()); System.out.printf( "环绕后置增强 result: %s%n" , result); return result; } |
- 装配
1
|
<aop:around method= "around" arg-names= "point" pointcut-ref= "pointcut" /> |
环绕通知可以实现任何通知的效果, 甚至可以阻止目标方法的执行.
抛出通知
- 定义通知
1
2
3
4
5
6
7
8
|
private static final Logger LOGGER = LoggerFactory.getLogger(Aspect. class ); public void afterThrowing(JoinPoint point, Throwable ex) { String message = new StringBuilder( "method " ).append(point.getSignature().getName()).append( " error" ).toString(); System.out.println(message); LOGGER.error( "{}," , message, ex); } |
- 装配
1
|
<aop:after-throwing method= "afterThrowing" throwing= "ex" pointcut-ref= "pointcut" /> |
throwing
属性指定异常对象名, 该名称应和方法定义参数名一致.
最终通知
- 定义通知
1
2
3
|
public void after(JoinPoint point) { System.out.println( "最终通知, 释放资源" ); } |
- 装配
1
|
<aop:after method= "after" pointcut-ref= "pointcut" /> |
无论目标方法是否出现异常,该通知都会执行(类似
finally
代码块, 应用场景为释放资源).
AspectJ-Annotation-AOP
@AspectJ
是AspectJ 1.5新增功能,可以通过JDK注解技术,直接在Bean类中定义切面.
AspectJ预定义的注解有:@Before
/@AfterReturning
/@Around
/@AfterThrowing
/@DeclareParents
/@After
.描述同前.
使用AspectJ注解AOP需要在applicationContext.xml文件中开启注解自动代理功能:
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
|
<?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:aop= "http://www.springframework.org/schema/aop" xmlns:context= "http://www.springframework.org/schema/context" 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 http: //www.springframework.org/schema/context http: //www.springframework.org/schema/context/spring-context.xsd"> <!-- 批量扫描 @Component --> <context:component-scan base- package = "com.fq" /> <!-- 启用注解自动代理 @Aspect --> <aop:aspectj-autoproxy/> </beans> |
OrderService
/Client
同前
@Before
- Aspect
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/** * @Aspect: 指定是一个切面 * @Component: 指定可以被Spring容器扫描到 */ @Aspect @Component public class CustomAspect { @Before ( "execution(* com.fq.service.impl.OrderServiceImpl.*(..))" ) public void before(JoinPoint point) { System.out.printf( "前置增强before2 %s%n" , point.getKind()); } } |
@AfterReturning
1
2
3
4
|
@AfterReturning (value = "execution(* com.fq.service.impl.OrderServiceImpl.d*(..))" , returning = "result" ) public void afterReturning(JoinPoint point, Object result) { System.out.printf( "后置增强, 结果为 %s%n" , result); } |
@Around
1
2
3
4
5
6
7
8
9
10
|
@Around ( "execution(* com.fq.service.impl.OrderServiceImpl.*(..))" ) public Object around(ProceedingJoinPoint point) throws Throwable { long start = System.currentTimeMillis(); Object result = point.proceed(point.getArgs()); long time = System.currentTimeMillis() - start; System.out.printf( "method %s invoke consuming %d ms%n" , point.toLongString(), time); return result; } |
如果不调用
ProceedingJoinPoint
的proceed
方法,那么目标方法就不执行了.
@AfterThrowing
1
2
3
4
5
6
7
|
@AfterThrowing (value = "execution(* com.fq.service.impl.OrderServiceImpl.*(..))" , throwing = "ex" ) public void afterThrowing(JoinPoint point, Throwable ex) { String message = new StringBuilder( "method " ).append(point.getSignature().getName()).append( " error" ).toString(); System.out.println(message); LOGGER.error( "{}," , message, ex); } |
@After
1
2
3
4
|
@After ( "execution(* com.fq.service.impl.OrderServiceImpl.*(..))" ) public void after(JoinPoint point) { System.out.println( "最终通知, 释放资源" ); } |
@Pointcut定义切点
对于重复的切点,可以使用@Pointcut
进行定义, 然后在通知注解内引用.
- 定义切点方法
无参/无返回值/方法名为切点名:
1
2
3
4
5
6
7
8
9
10
|
/** * @author jifang * @since 16/3/4 上午11:47. */ public class OrderServicePointcut { @Pointcut ( "execution(* com.fq.service.impl.OrderServiceImpl.*(..))" ) public void pointcut() { } } |
- 引用切点
在Advice上像调用方法一样引用切点:
1
2
3
4
|
@After ( "OrderServicePointcut.pointcut()" ) public void after(JoinPoint point) { System.out.println( "最终通知, 释放资源" ); } |
1) 如果切点与切面在同一个类内, 可省去类名前缀;
2) 当需要通知多个切点时,可以使用||
/&&
进行连接.
小结
通知 | 描述 |
---|---|
前置通知 | 权限控制(少用) |
后置通知 | 少用 |
环绕通知 | 权限控制/性能监控/缓存实现/事务管理 |
异常通知 | 发生异常后,记录错误日志 |
最终通知 | 释放资源 |