AOP 那点事儿

今天我要和你们分享的是 AOP(Aspect-Oriented Programming)这个东西,名字与 OOP 仅差一个字母,其实它是对 OOP 编程方式的一种补充,并不是是取而代之。翻译过来就是“面向方面编程”,可我更倾向于翻译为“面向切面编程”。它听起有些的神秘,为何呢?当你看完这篇文章的时候,就就知道,咱们作的很重要的工做就是去写这个“切面” 。那么什么是“切面”呢? 程序员

没错!就是用一把刀来切一坨面。注意,相对于面而言,咱们必定是横着来切它,这简称为“横切”。能够把一段代码想象成一坨面,一样也能够用一把刀来横切它,下面要作的就是如何去实现这把刀! spring

须要澄清的是,这个概念不是由 Rod Johnson(老罗)提出的。其实很早之前就有了,目前最知名最强大的 Java 开源项目就是 AspectJ 了,然而它的前身是 AspectWerkz(该项目已经在 2005 年中止更新),这才是 AOP 的老祖宗。老罗(一个头发秃得和我老爸有一拼的天才)写了一个叫作 Spring 框架,今后一炮走红,成为了 Spring 之父。他在本身的 IOC 的基础之上,又实现了一套 AOP 的框架,后来仿佛发现本身愈来愈走进深渊里,在不能自拔的时候,有人建议他仍是集成 AspectJ 吧,他在万般无奈之下才接受了该建议。因而,咱们如今用得最多的想必就是 Spring + AspectJ 这种 AOP 框架了。 数据库

那么 AOP 究竟是什么?如何去使用它?本文将逐步带您进入 AOP 的世界,让您感觉到史无前例的畅快! 编程

不过在开始讲解 AOP 以前,我想有必要回忆一下这段代码: 架构

1. 写死代码 框架

先来一个接口: ide

1 publicinterfaceGreeting {
2  
3     voidsayHello(String name);
4 }
还有一个实现类:
01 publicclassGreetingImplimplementsGreeting {
02  
03     @Override
04     publicvoidsayHello(String name) {
05         before();
06         System.out.println("Hello! "+ name);
07         after();
08     }
09  
10     privatevoidbefore() {
11         System.out.println("Before");
12     }
13  
14     privatevoidafter() {
15         System.out.println("After");
16     }
17 }

before() 与 after() 方法写死在 sayHello() 方法体中了,这样的代码的味道很是很差。若是哪位仁兄大量写了这样的代码,确定要被你的架构师骂个够呛。 性能

好比:咱们要统计每一个方法的执行时间,以对性能做出评估,那是否是要在每一个方法的一头一尾都作点手脚呢? this

再好比:咱们要写一个 JDBC 程序,那是否是也要在方法的开头去链接数据库,方法的末尾去关闭数据库链接呢? spa

这样的代码只会把程序员累死,把架构师气死!

必定要想办法对上面的代码进行重构,首先给出三个解决方案:

2. 静态代理

最简单的解决方案就是使用静态代理模式了,咱们单独为 GreetingImpl 这个类写一个代理类:

01 publicclassGreetingProxyimplementsGreeting {
02  
03     privateGreetingImpl greetingImpl;
04  
05     publicGreetingProxy(GreetingImpl greetingImpl) {
06         this.greetingImpl = greetingImpl;
07     }
08  
09     @Override
10     publicvoidsayHello(String name) {
11         before();
12         greetingImpl.sayHello(name);
13         after();
14     }
15  
16     privatevoidbefore() {
17         System.out.println("Before");
18     }
19  
20     privatevoidafter() {
21         System.out.println("After");
22     }
23 }
就用这个 GreetingProxy 去代理 GreetingImpl,下面看看客户端如何来调用:
1 publicclassClient {
2  
3     publicstaticvoidmain(String[] args) {
4         Greeting greetingProxy =newGreetingProxy(newGreetingImpl());
5         greetingProxy.sayHello("Jack");
6     }
7 }

这样写没错,可是有个问题,XxxProxy 这样的类会愈来愈多,如何才能将这些代理类尽量减小呢?最好只有一个代理类。

这时咱们就须要使用 JDK 提供的动态代理了。 

3. JDK 动态代理

01 publicclassJDKDynamicProxyimplementsInvocationHandler {
02  
03     privateObject target;
04  
05     publicJDKDynamicProxy(Object target) {
06         this.target = target;
07     }
08  
09     @SuppressWarnings("unchecked")
10     public<T> T getProxy() {
11         return(T) Proxy.newProxyInstance(
12             target.getClass().getClassLoader(),
13             target.getClass().getInterfaces(),
14             this
15         );
16     }
17  
18     @Override
19     publicObject invoke(Object proxy, Method method, Object[] args)throwsThrowable {
20         before();
21         Object result = method.invoke(target, args);
22         after();
23         returnresult;
24     }
25  
26     privatevoidbefore() {
27         System.out.println("Before");
28     }
29  
30     privatevoidafter() {
31         System.out.println("After");
32     }
33 }
客户端是这样调用的:
1 publicclassClient {
2  
3     publicstaticvoidmain(String[] args) {
4         Greeting greeting =newJDKDynamicProxy(newGreetingImpl()).getProxy();
5         greeting.sayHello("Jack");
6     }
7 }

这样全部的代理类都合并到动态代理类中了,但这样作仍然存在一个问题:JDK 给咱们提供的动态代理只能代理接口,而不能代理没有接口的类。有什么方法能够解决呢?

4. CGLib 动态代理

咱们使用开源的 CGLib 类库能够代理没有接口的类,这样就弥补了 JDK 的不足。CGLib 动态代理类是这样玩的:

01 publicclassCGLibDynamicProxyimplementsMethodInterceptor {
02  
03     privatestaticCGLibDynamicProxy instance =newCGLibDynamicProxy();
04  
05     privateCGLibDynamicProxy() {
06     }
07  
08     publicstaticCGLibDynamicProxy getInstance() {
09         returninstance;
10     }
11  
12     @SuppressWarnings("unchecked")
13     public<T> T getProxy(Class<T> cls) {
14         return(T) Enhancer.create(cls,this);
15     }
16  
17     @Override
18     publicObject intercept(Object target, Method method, Object[] args, MethodProxy proxy)throwsThrowable {
19         before();
20         Object result = proxy.invokeSuper(target, args);
21         after();
22         returnresult;
23     }
24  
25     privatevoidbefore() {
26         System.out.println("Before");
27     }
28  
29     privatevoidafter() {
30         System.out.println("After");
31     }
32 }
以上代码中了 Singleton 模式,那么客户端调用也更加轻松了:
1 publicclassClient {
2  
3     publicstaticvoidmain(String[] args) {
4         Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class);
5         greeting.sayHello("Jack");
6     }
7 }

到此为止,咱们能作的都作了,问题彷佛所有都解决了。但事情总不会那么完美,而咱们必定要追求完美!

老罗搞出了一个 AOP 框架,可否作到完美而优雅呢?请你们继续往下看吧!

5. Spring AOP:前置加强、后置加强、环绕加强(编程式)

在 Spring AOP 的世界里,与 AOP 相关的术语实在太多,每每也是咱们的“拦路虎”,不论是看那本书或是技术文档,在开头都要将这些术语逐个灌输给读者。我想这彻底是在吓唬人了,其实没那么复杂的,你们放轻松一点。

咱们上面例子中提到的 before() 方法,在 Spring AOP 里就叫 Before Advice(前置加强)。有些人将 Advice 直译为“通知”,我想这是不太合适的,由于它根本就没有“通知”的含义,而是对原有代码功能的一种“加强”。再说,CGLib 中也有一个 Enhancer 类,它就是一个加强类。

此外,像 after() 这样的方法就叫 After Advice(后置加强),由于它放在后面来加强代码的功能

若是能把 before() 与 after() 合并在一块儿,那就叫 Around Advice(环绕加强),就像汉堡同样,中间夹一根火腿。

这三个概念是否是轻松地理解了呢?若是是,那就继续吧!

咱们下面要作的就是去实现这些所谓的“加强类”,让他们横切到代码中,而不是将这些写死在代码中。

先来一个前置加强类吧:

1 publicclassGreetingBeforeAdviceimplementsMethodBeforeAdvice {
2  
3     @Override
4     publicvoidbefore(Method method, Object[] args, Object target)throwsThrowable {
5         System.out.println("Before");
6     }
7 }

注意:这个类实现了 org.springframework.aop.MethodBeforeAdvice 接口,咱们将须要加强的代码放入其中。

再来一个后置加强类吧:

1 publicclassGreetingAfterAdviceimplementsAfterReturningAdvice {
2  
3     @Override
4     publicvoidafterReturning(Object result, Method method, Object[] args, Object target)throwsThrowable {
5         System.out.println("After");
6     }
7 }

相似地,这个类实现了 org.springframework.aop.AfterReturningAdvice 接口。

最后用一个客户端来把它们集成起来,看看如何调用吧:

01 publicclassClient {
02  
03     publicstaticvoidmain(String[] args) {
04         ProxyFactory proxyFactory =newProxyFactory();    // 建立代理工厂
05         proxyFactory.setTarget(newGreetingImpl());        // 射入目标类对象
06         proxyFactory.addAdvice(newGreetingBeforeAdvice());// 添加前置加强
07         proxyFactory.addAdvice(newGreetingAfterAdvice()); // 添加后置加强
08  
09         Greeting greeting = (Greeting) proxyFactory.getProxy();// 从代理工厂中获取代理
10         greeting.sayHello("Jack");                             // 调用代理的方法
11     }
12 }

请仔细阅读以上代码及其注释,您会发现,其实 Spring AOP 仍是挺简单的,对吗?

固然,咱们彻底能够只定义一个加强类,让它同时实现 MethodBeforeAdvice 与 AfterReturningAdvice 这两个接口,以下:

01 publicclassGreetingBeforeAndAfterAdviceimplementsMethodBeforeAdvice, AfterReturningAdvice {
02  
03     @Override
04     publicvoidbefore(Method method, Object[] args, Object target)throwsThrowable {
05         System.out.println("Before");
06     }
07  
08     @Override
09     publicvoidafterReturning(Object result, Method method, Object[] args, Object target)throwsThrowable {
10         System.out.println("After");
11     }
12 }
这样咱们只须要使用一行代码,同时就能够添加前置与后置加强:
1 proxyFactory.addAdvice(newGreetingBeforeAndAfterAdvice());

刚才有提到“环绕加强”,其实这个东西能够把“前置加强”与“后置加强”的功能给合并起来,无需让咱们同时实现以上两个接口。

01 publicclassGreetingAroundAdviceimplementsMethodInterceptor {
02  
03     @Override
04     publicObject invoke(MethodInvocation invocation)throwsThrowable {
05         before();
06         Object result = invocation.proceed();
07         after();
08         returnresult;
09     }
10  
11     privatevoidbefore() {
12         System.out.println("Before");
13     }
14  
15     privatevoidafter() {
16         System.out.println("After");
17     }
18 }

环绕加强类须要实现 org.aopalliance.intercept.MethodInterceptor 接口。注意,这个接口不是 Spring 提供的,它是 AOP 联盟(一个很牛逼的联盟)写的,Spring 只是借用了它。

在客户端中一样也须要将该加强类的对象添加到代理工厂中:

1 proxyFactory.addAdvice(newGreetingAroundAdvice());

好了,这就是 Spring AOP 的基本用法,但这只是“编程式”而已。Spring AOP 若是只是这样,那就太傻逼了,它曾经也是一度宣传用 Spring 配置文件的方式来定义 Bean 对象,把代码中的 new 操做所有解脱出来。

6.   Spring AOP:前置加强、后置加强、环绕加强(声明式)

先看 Spring 配置文件是如何写的吧:

01 <?xmlversion="1.0"encoding="UTF-8"?>
02 <beansxmlns="http://www.springframework.org/schema/beans"
03        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04        xmlns:context="http://www.springframework.org/schema/context"
05        xsi:schemaLocation="http://www.springframework.org/schema/beans
06        http://www.springframework.org/schema/beans/spring-beans.xsd
07        http://www.springframework.org/schema/context
08        http://www.springframework.org/schema/context/spring-context.xsd">
09  
10     <!-- 扫描指定包(将 @Component 注解的类自动定义为 Spring Bean) -->
11     <context:component-scanbase-package="aop.demo"/>
12  
13     <!-- 配置一个代理 -->
14     <beanid="greetingProxy"class="org.springframework.aop.framework.ProxyFactoryBean">
15         <propertyname="interfaces"value="aop.Greeting"/><!-- 须要代理的接口 -->
16         <propertyname="target"ref="greetingImpl"/>      <!-- 接口实现类 -->
17         <propertyname="interceptorNames">                <!-- 拦截器名称(也就是加强类名称,Spring Bean 的 id) -->
18             <list>
19                 <value>greetingAroundAdvice</value>
20             </list>
21         </property>
22     </bean>
23  
24 </beans>

必定要阅读以上代码的注释,其实使用 ProxyFactoryBean 就能够取代前面的 ProxyFactory,其实它们俩就一回事儿。我认为 interceptorNames 应该更名为 adviceNames 或许会更容易让人理解,不就是往这个属性里面添加加强类吗?

此外,若是只有一个加强类,可使用如下方法来简化:

1 ...
2  
3     <beanid="greetingProxy"class="org.springframework.aop.framework.ProxyFactoryBean">
4         <propertyname="interfaces"value="aop.Greeting"/>
5         <propertyname="target"ref="greetingImpl"/>
6         <propertyname="interceptorNames"value="greetingAroundAdvice"/><!-- 注意这行配置 -->
7     </bean>
8  
9 ...

还须要注意的是,这里使用了 Spring 2.5+ 的特性“Bean 扫描”,这样咱们就无需在 Spring 配置文件里不断地定义 <bean id="xxx" class="xxx"/> 了,从而解脱了咱们的双手。

看看这是有多么的简单:

1 @Component
2 publicclassGreetingImplimplementsGreeting {
3  
4     ...
5 }
1 @Component
2 publicclassGreetingAroundAdviceimplementsMethodInterceptor {
3  
4     ...
5 }
最后看看客户端吧:
1 publicclassClient {
2  
3     publicstaticvoidmain(String[] args) {
4         ApplicationContext context =newClassPathXmlApplicationContext("aop/demo/spring.xml");// 获取 Spring Context
5         Greeting greeting = (Greeting) context.getBean("greetingProxy");                       // 从 Context 中根据 id 获取 Bean 对象(其实就是一个代理)
6         greeting.sayHello("Jack");                                                             // 调用代理的方法
7     }
8 }

代码量确实少了,咱们将配置性的代码放入配置文件,这样也有助于后期维护。更重要的是,代码只关注于业务逻辑,而将配置放入文件中。这是一条最佳实践!

除了上面提到的那三类加强之外,其实还有两类加强也须要了解一下,关键的时候您要能想获得它们才行。 

7. Spring AOP:抛出加强

程序报错,抛出异常了,通常的作法是打印到控制台或日志文件中,这样不少地方都得去处理,有没有一个一劳永逸的方法呢?那就是 Throws Advice(抛出加强),它确实很强,不信你就继续往下看:

01 @Component
02 publicclassGreetingImplimplementsGreeting {
03  
04     @Override
05     publicvoidsayHello(String name) {
06         System.out.println("Hello! "+ name);
07  
08         thrownewRuntimeException("Error");// 故意抛出一个异常,看看异常信息可否被拦截到
09     }
10 }

下面是抛出加强类的代码:

01 @Component
02 publicclassGreetingThrowAdviceimplementsThrowsAdvice {
03  
04     publicvoidafterThrowing(Method method, Object[] args, Object target, Exception e) {
05         System.out.println("---------- Throw Exception ----------");
06         System.out.println("Target Class: "+ target.getClass().getName());
07         System.out.println("Method Name: "+ method.getName());
08         System.out.println("Exception Message: "+ e.getMessage());
09         System.out.println("-------------------------------------");
10     }
11 }

抛出加强类须要实现 org.springframework.aop.ThrowsAdvice 接口,在接口方法中可获取方法、参数、目标对象、异常对象等信息。咱们能够把这些信息统一写入到日志中,固然也能够持久化到数据库中。

这个功能确实太棒了!但还有一个更厉害的加强。若是某个类实现了 A 接口,但没有实现 B 接口,那么该类能够调用 B 接口的方法吗?若是您没有看到下面的内容,必定不敢相信原来这是可行的!

8. Spring AOP:引入加强

以上提到的都是对方法的加强,那可否对类进行加强呢?用 AOP 的行话来说,对方法的加强叫作 Weaving(织入),而对类的加强叫作 Introduction(引入)。而 Introduction Advice(引入加强)就是对类的功能加强,它也是 Spring AOP 提供的最后一种加强。建议您一开始千万不要去看《Spring Reference》,不然您必定会后悔的。由于当您看了如下的代码示例后,必定会完全明白什么才是引入加强。

定义了一个新接口 Apology(道歉):

1 publicinterfaceApology {
2  
3     voidsaySorry(String name);
4 }

但我不想在代码中让 GreetingImpl 直接去实现这个接口,我想在程序运行的时候动态地实现它。由于假如我实现了这个接口,那么我就必定要改写 GreetingImpl 这个类,关键是我不想改它,或许在真实场景中,这个类有1万行代码,我实在是不敢动了。因而,我须要借助 Spring 的引入加强。这个有点意思了!

01 @Component
02 publicclassGreetingIntroAdviceextendsDelegatingIntroductionInterceptorimplementsApology {
03  
04    @Override
05    publicObject invoke(MethodInvocation invocation)throwsThrowable {
06        returnsuper.invoke(invocation);
07    }
08  
09     @Override
10     publicvoidsaySorry(String name) {
11         System.out.println("Sorry! "+ name);
12     }
13 }

以上定义了一个引入加强类,扩展 了 org.springframework.aop.support.DelegatingIntroductionInterceptor 类,同时也实现了新定义的 Apology 接口。在类中首先覆盖了父类的 invoke() 方法,而后实现了 Apology 接口的方法。我就是想用这个加强类去丰富 GreetingImpl 类的功能,那么这个 GreetingImpl 类无需直接实现 Apology 接口,就能够在程序运行的时候调用 Apology 接口的方法了。这简直是太神奇的!

看看是如何配置的吧:

01 <?xmlversion="1.0"encoding="UTF-8"?>
02 <beansxmlns="http://www.springframework.org/schema/beans"
03        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04        xmlns:context="http://www.springframework.org/schema/context"
05        xsi:schemaLocation="http://www.springframework.org/schema/beans
06        http://www.springframework.org/schema/beans/spring-beans.xsd
07        http://www.springframework.org/schema/context
08        http://www.springframework.org/schema/context/spring-context.xsd">
09  
10     <context:component-scanbase-package="aop.demo"/>
11  
12     <beanid="greetingProxy"class="org.springframework.aop.framework.ProxyFactoryBean">
13         <propertyname="interfaces"value="aop.demo.Apology"/>         <!-- 须要动态实现的接口 -->
14         <propertyname="target"ref="greetingImpl"/>                   <!-- 目标类 -->
15         <propertyname="interceptorNames"value="greetingIntroAdvice"/><!-- 引入加强 -->
16         <propertyname="proxyTargetClass"value="true"/>               <!-- 代理目标类(默认为 false,代理接口) -->
17     </bean>
18  
19 </beans>

须要注意 proxyTargetClass 属性,它代表是否代理目标类,默认为 false,也就是代理接口了,此时 Spring 就用 JDK 动态代理。若是为 true,那么 Spring 就用 CGLib 动态代理。这简直就是太方便了!Spring 封装了这一切,让程序员不在关心那么多的细节。咱们要向老罗同志致敬,您是咱们心中永远的 idol!

当您看完下面的客户端代码,必定会彻底明白以上的这一切:

01 publicclassClient {
02  
03     publicstaticvoidmain(String[] args) {
04         ApplicationContext context =newClassPathXmlApplicationContext("aop/demo/spring.xml");
05         GreetingImpl greetingImpl = (GreetingImpl) context.getBean("greetingProxy");// 注意:转型为目标类,而并不是它的 Greeting 接口
06         greetingImpl.sayHello("Jack");
07  
08         Apology apology = (Apology) greetingImpl;// 将目标类强制向上转型为 Apology 接口(这是引入加强给咱们带来的特性,也就是“接口动态实现”功能)
09         apology.saySorry("Jack");
10     }
11 }

没想到 saySorry() 方法原来是能够被 greetingImpl 对象来直接调用的,只需将其强制转换为该接口便可。

咱们再次感谢 Spring AOP,感谢老罗给咱们提供了这么强大的特性!

其实,Spring AOP 还有不少精彩的地方,下一篇将介绍更多更有价值的 AOP 技术,让你们获得更多的收获。

未完,待续...

相关文章
相关标签/搜索