前言
容器和AOP是Spring的两大核心。本文将来学习Spring AOP。
AOP是什么?
AOP在计算机科学领域还是相对年轻的概念,由Xerox PARC公司发明。Gregor Kiczales 在1997年领导一队研究人员首次介绍了AOP。当时他们关心的问题是如何在大型面向对象的代码库中重复使用那些必要且代价高的样板,那些样板的通用例子具有日志,缓存和事务功能。在最终的研究报告中,Kiczales和他的团队描述了OOP技术不能捕获和解决的问题,他们发现横切关注点最终分散在整个代码中,这种交错的代码会变得越来越难开发和维护。他们分析了所有技术原因,包括为何这种纠缠模式会出现,为什么避免起来这么困难,甚至涉及了设计模式的正确使用。该报告描述了一种解决方案作为OOP的补充,即使用“切面aspects”封装横切关注点以及允许重复使用。最终实现了AspectJ,就是今天Java开发者仍然使用的一流AOP工具。
也就是说,AOP可不是Spring发明的,Spring只是对AOP做了支持而已。既然如此,AOP里面的几个概念就是通用的了。
《Spring in Action》这本书给出了明确的解释:
在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的。比如:日志、声明式事物、安全和缓存。这些东西都不是我们平时写代码的核心功能,但许多地方都要用到。
把这些横切关注点与业务相分离正是面向切面编程(AOP)索要解决的问题。
简单的说就是把这些许多地方都要用到,但又不是核心业务的功能,单独剥离出来封装,通过配置指定要切入到指定的方法中去。
如上图所示,这就是横切关注点的概念,水平的是核心业务,这些切入的箭头就是我们的横切关注点。
横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处:
首先,现在每个关注点都集中于一个地方,而不是分割到多处代码中
其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。
AOP术语
通知(Advice):
在AOP中,切面的工作被称为通知。通知定义了切面“是什么”以及“何时”使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
- 返回通知(After-returning):在目标方法成功执行之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
连接点(Join point):
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加行为。
切点(Pointcut):
如果说通知定义了切面“是什么”和“何时”的话,那么切点就定义了“何处”。比如我想把日志引入到某个具体的方法中,这个方法就是所谓的切点。
切面(Aspect):
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容———他是什么,在何时和何处完成其功能。
引入(Introduction):
引入允许我们向现有的类添加新的方法和属性(Spring提供了一个方法注入的功能)。
织入(Weaving):
把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:
- 编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器。
- 类加载时:使用特殊的 ClassLoader 在目标类被加载到程序之前增强类的字节代码。
- 运行时:切面在运行的某个时刻被织入, 方式是容器为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring对AOP的支持
同依赖注入一样,Spring AOP也提供两种配置:
- 基于Java Annotation;
- 基于Xml配置;
Spring AOP是基于动态代理实现的,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。至于什么是动态代理,后文将会解释。
动态代理只能处理方法,因此Spring AOP只支持方法连接点。但是通常方法拦截可以满足大部分需求。如果需要字段或构造器拦截,别忘了Spring是一个高度可扩展的框架,可以使用AspectJ 等第三方AOP框架。
下面就举两个例子分别来介绍下Spring AOP的两种配置方式,我们就拿简单的日志来说明。
使用Java注解配置
@Component //声明bean
@Aspect //该注解标示该类为切面类
public class LogAspect {
@Pointcut("execution(* com.springdemo.service.impl.UserServiceImpl.*(..))")
public void logAop(){}
@Before("logAop() && args(name)")
public void logBefore(String name){
System.out.println(name+"前置通知Before");
}
@AfterReturning("logAop()")
public void logAfterReturning(){
System.out.println("返回通知AfterReturning");
}
@After("logAop() && args(name)")
public void logAfter(String name){
System.out.println(name+"后置通知After");
}
@AfterThrowing("logAop()")
public void logAfterThrow(){
System.out.println("异常通知AfterThrowing");
}
}
该代码片段就声明了一个bean,而这个bean是一个切面,其中的方法将会应用于com.springdemo.service.impl.UserServiceImpl
类的任何方法。
UserServiceImpl
类很简单,只是个普通的bean:
package com.springdemo.service.impl;
@Service("userService")
public class UserServiceImpl implements UserService{
@Override
public void sayHello(String name) {
System.out.println("hello, "+name);
}
}
最后,还要启用Spring AOP的注解配置。同样采用Java配置:
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class Config{
...
}
注意不要被注解里的“AspectJ”字样吓到了,这个注解确实是AspectJ定义的,但Spring只是重用了这个注解,实现还是Spring AOP自己的基于动态代理的实现。
sayHello
被执行后的结果:
aop前置通知Before
hello, aop
aop后置通知After
返回通知AfterReturning
本段代码源自嘟嘟的博客,里面有具体的解释。
基于XML的配置
同理,不再列出详细代码。
-
<aop:aspectj-autoproxy/>
启用AOP注解。 - 如不采用注解,则用
aop
名称空间的标记描述切面。如下是一个等效的配置:
<bean id="logAspect" class="com.springdemo.aspect.LogAspect" />
<aop:config>
<aop:aspect id="log" ref="logAspect">
<aop:pointcut id="logAop" expression="execution(* com.springdemo.service.impl.UserServiceImpl.sayHello(..)) and args(name)"/>
<aop:before method="logBefore" pointcut-ref="logAop"/>
<aop:after method="logAfter" pointcut-ref="logAop"/>
<aop:after-returning method="logAfterReturning" pointcut-ref="logAop"/>
<aop:after-throwing method="logAfterThrow" pointcut-ref="logAop"/>
<!--<aop:around method="logAfterThrow" pointcut-ref="logAop"/>-->
</aop:aspect>
</aop:config>
Spring AOP原理 —— 动态代理
参考这篇知乎问答。
首先,静态代理应该是普遍了解的概念了。它是OOP的一种设计模式,依赖于接口。
动态代理则是为了解决目标类方法越来越多时,代理类也要跟着膨胀的问题。因为动态代理类只要在invoke()
方法中有选择地实现接口的方法。这对那些普遍适用的功能来说特别适合,比如缓存、认证、log等等。
public class ProxyHandler implements InvocationHandler
{
private Object tar;
//绑定委托对象,并返回代理类
public Object bind(Object tar)
{
this.tar = tar;
//绑定该类实现的所有接口,取得代理类
return Proxy.newProxyInstance(tar.getClass().getClassLoader(),
tar.getClass().getInterfaces(),
this);
}
// invoke中的逻辑可以对接口中的所有方法生效。
public Object invoke(Object proxy , Method method , Object[] args)throws Throwable
{
Object result = null;
//在调用具体函数方法前,执行功能处理
result = method.invoke(tar,args);
//在调用具体函数方法后,执行功能处理
return result;
}
}
至于InvocationHandler
,它是JDK的java.lang.reflect
名称空间里提供的一个类,用来实现动态代理。具体可见Java Dynamic Proxy API。
可以看出,动态代理的功能十分强大,因此得到了广泛的应用。比如单元测试中的mock框架,MyBatis的sql注解等等。
最后,动态代理仍是需要接口的。而借助另一个第三方类库CGLib,则可以动态生成字节码,不依赖于接口。Spring AOP对这两种技术都有使用。参见CSDN。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。