AOP(Aspect-Oriented Programming)这个东西,名字与 OOP 仅差一个字母,其实它是对 OOP 编程方式的一种补充,并不是是取而代之。翻译过来就是“面向方面编程”,可我更倾向于翻译为“面向切面编程”。它听起有些的神秘,为何呢?当你看完这篇文章的时候,就会知道,咱们作的很重要的工做就是去写这个“切面” 。那么什么是“切面”呢?java
没错!就是用一把刀来切一坨面。注意,相对于面而言,咱们必定是横着来切它,这简称为“横切”。能够把一段代码想象成一坨面,一样也能够用一把刀来横切它,下面要作的就是如何去实现这把刀!程序员
须要澄清的是,这个概念不是由 Rod Johnson(老罗)提出的。其实很早之前就有了,目前最知名最强大的 Java 开源项目就是 AspectJ 了,然而它的前身是 AspectWerkz(该项目已经在 2005 年中止更新),这才是 AOP 的老祖宗。老罗(一个头发秃得和我老爸有一拼的天才)写了一个叫作 Spring 框架,今后一炮走红,成为了 Spring 之父。他在本身的 IOC 的基础之上,又实现了一套 AOP 的框架,后来仿佛发现本身愈来愈走进深渊里,在不能自拔的时候,有人建议他仍是集成 AspectJ 吧,他在万般无奈之下才接受了该建议。因而,咱们如今用得最多的想必就是 Spring + AspectJ 这种 AOP 框架了。正则表达式
那么 AOP 究竟是什么?如何去使用它?本文将逐步带您进入 AOP 的世界,让您感觉到史无前例的畅快!spring
不过在开始讲解 AOP 以前,我想有必要回忆一下这段代码:数据库
1. 写死代码编程
先来一个接口:架构
public interface Greeting { void sayHello(String name); }
还有一个实现类:框架
public class GreetingImpl implements Greeting { @Override public void sayHello(String name) { before(); System.out.println("Hello! " + name); after(); } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
before() 与 after() 方法写死在 sayHello() 方法体中了,这样的代码的味道很是很差。若是哪位仁兄大量写了这样的代码,确定要被你的架构师骂个够呛。ide
好比:咱们要统计每一个方法的执行时间,以对性能做出评估,那是否是要在每一个方法的一头一尾都作点手脚呢?工具
再好比:咱们要写一个 JDBC 程序,那是否是也要在方法的开头去链接数据库,方法的末尾去关闭数据库链接呢?
这样的代码只会把程序员累死,把架构师气死!
必定要想办法对上面的代码进行重构,首先给出三个解决方案:
2. 静态代理
最简单的解决方案就是使用静态代理模式了,咱们单独为 GreetingImpl 这个类写一个代理类:
public class GreetingProxy implements Greeting { private GreetingImpl greetingImpl; public GreetingProxy(GreetingImpl greetingImpl) { this.greetingImpl = greetingImpl; } @Override public void sayHello(String name) { before(); greetingImpl.sayHello(name); after(); } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
就用这个 GreetingProxy 去代理 GreetingImpl,下面看看客户端如何来调用:
public class Client { public static void main(String[] args) { Greeting greetingProxy = new GreetingProxy(new GreetingImpl()); greetingProxy.sayHello("Jack"); } }
这样写没错,可是有个问题,XxxProxy 这样的类会愈来愈多,如何才能将这些代理类尽量减小呢?最好只有一个代理类。
这时咱们就须要使用 JDK 提供的动态代理了。
3. JDK 动态代理
public class JDKDynamicProxy implements InvocationHandler { private Object target; public JDKDynamicProxy(Object target) { this.target = target; } @SuppressWarnings("unchecked") public <T> T getProxy() { return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), this ); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(target, args); after(); return result; } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
客户端是这样调用的:
public class Client { public static void main(String[] args) { Greeting greeting = new JDKDynamicProxy(new GreetingImpl()).getProxy(); greeting.sayHello("Jack"); } }
这样全部的代理类都合并到动态代理类中了,但这样作仍然存在一个问题:JDK 给咱们提供的动态代理只能代理接口,而不能代理没有接口的类。有什么方法能够解决呢?
4. CGLib 动态代理
咱们使用开源的 CGLib 类库能够代理没有接口的类,这样就弥补了 JDK 的不足。CGLib 动态代理类是这样玩的:
public class CGLibDynamicProxy implements MethodInterceptor { private static CGLibDynamicProxy instance = new CGLibDynamicProxy(); private CGLibDynamicProxy() { } public static CGLibDynamicProxy getInstance() { return instance; } @SuppressWarnings("unchecked") public <T> T getProxy(Class<T> cls) { return (T) Enhancer.create(cls, this); } @Override public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable { before(); Object result = proxy.invokeSuper(target, args); after(); return result; } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
以上代码中了 Singleton 模式,那么客户端调用也更加轻松了:
public class Client { public static void main(String[] args) { Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl.class); greeting.sayHello("Jack"); } }
到此为止,咱们能作的都作了,问题彷佛所有都解决了。但事情总不会那么完美,而咱们必定要追求完美!
老罗搞出了一个 AOP 框架,可否作到完美而优雅呢?请你们继续往下看吧!
5. Spring AOP:前置加强、后置加强、环绕加强(编程式)
在 Spring AOP 的世界里,与 AOP 相关的术语实在太多,每每也是咱们的“拦路虎”,无论是看那本书或是技术文档,在开头都要将这些术语逐个灌输给读者。我想这彻底是在吓唬人了,其实没那么复杂的,你们放轻松一点。
咱们上面例子中提到的 before() 方法,在 Spring AOP 里就叫 Before Advice(前置加强)。有些人将 Advice 直译为“通知”,我想这是不太合适的,由于它根本就没有“通知”的含义,而是对原有代码功能的一种“加强”。再说,CGLib 中也有一个 Enhancer 类,它就是一个加强类。
此外,像 after() 这样的方法就叫 After Advice(后置加强),由于它放在后面来加强代码的功能。
若是能把 before() 与 after() 合并在一块儿,那就叫 Around Advice(环绕加强),就像汉堡同样,中间夹一根火腿。
这三个概念是否是轻松地理解了呢?若是是,那就继续吧!
咱们下面要作的就是去实现这些所谓的“加强类”,让他们横切到代码中,而不是将这些写死在代码中。
先来一个前置加强类吧:
public class GreetingBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("Before"); } }
注意:这个类实现了 org.springframework.aop.MethodBeforeAdvice 接口,咱们将须要加强的代码放入其中。
再来一个后置加强类吧:
public class GreetingAfterAdvice implements AfterReturningAdvice { @Override public void afterReturning(Object result, Method method, Object[] args, Object target) throws Throwable { System.out.println("After"); } }
相似地,这个类实现了 org.springframework.aop.AfterReturningAdvice 接口。
最后用一个客户端来把它们集成起来,看看如何调用吧:
public class Client { public static void main(String[] args) { ProxyFactory proxyFactory = new ProxyFactory(); // 建立代理工厂 proxyFactory.setTarget(new GreetingImpl()); // 射入目标类对象 proxyFactory.addAdvice(new GreetingBeforeAdvice()); // 添加前置加强 proxyFactory.addAdvice(new GreetingAfterAdvice()); // 添加后置加强 Greeting greeting = (Greeting) proxyFactory.getProxy(); // 从代理工厂中获取代理 greeting.sayHello("Jack"); // 调用代理的方法 } }
请仔细阅读以上代码及其注释,您会发现,其实 Spring AOP 仍是挺简单的,对吗?
固然,咱们彻底能够只定义一个加强类,让它同时实现 MethodBeforeAdvice 与 AfterReturningAdvice 这两个接口,以下:
public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice, AfterReturningAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("Before"); } @Override public void afterReturning(Object result, Method method, Object[] args, Object target) throws Throwable { System.out.println("After"); } }
这样咱们只须要使用一行代码,同时就能够添加前置与后置加强:
proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());
刚才有提到“环绕加强”,其实这个东西能够把“前置加强”与“后置加强”的功能给合并起来,无需让咱们同时实现以上两个接口。
public class GreetingAroundAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { before(); Object result = invocation.proceed(); after(); return result; } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
环绕加强类须要实现 org.aopalliance.intercept.MethodInterceptor 接口。注意,这个接口不是 Spring 提供的,它是 AOP 联盟(一个很牛逼的联盟)写的,Spring 只是借用了它。
在客户端中一样也须要将该加强类的对象添加到代理工厂中:
proxyFactory.addAdvice(new GreetingAroundAdvice());
好了,这就是 Spring AOP 的基本用法,但这只是“编程式”而已。Spring AOP 若是只是这样,那就太傻逼了,它曾经也是一度宣传用 Spring 配置文件的方式来定义 Bean 对象,把代码中的 new 操做所有解脱出来。
6. Spring AOP:前置加强、后置加强、环绕加强(声明式)
先看 Spring 配置文件是如何写的吧:
<?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" 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.xsd"> <!-- 扫描指定包(将 @Component 注解的类自动定义为 Spring Bean) --> <context:component-scan base-package="aop.demo"/> <!-- 配置一个代理 --> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="aop.Greeting"/> <!-- 须要代理的接口 --> <property name="target" ref="greetingImpl"/> <!-- 接口实现类 --> <property name="interceptorNames"> <!-- 拦截器名称(也就是加强类名称,Spring Bean 的 id) --> <list> <value>greetingAroundAdvice</value> </list> </property> </bean> </beans>
必定要阅读以上代码的注释,其实使用 ProxyFactoryBean 就能够取代前面的 ProxyFactory,其实它们俩就一回事儿。我认为 interceptorNames 应该更名为 adviceNames 或许会更容易让人理解,不就是往这个属性里面添加加强类吗?
此外,若是只有一个加强类,可使用如下方法来简化:
... <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="aop.Greeting"/> <property name="target" ref="greetingImpl"/> <property name="interceptorNames" value="greetingAroundAdvice"/> <!-- 注意这行配置 --> </bean> ...
还须要注意的是,这里使用了 Spring 2.5+ 的特性“Bean 扫描”,这样咱们就无需在 Spring 配置文件里不断地定义 <bean id=”xxx”/> 了,从而解脱了咱们的双手。
看看这是有多么的简单:
@Component public class GreetingImpl implements Greeting { ... }
@Component public class GreetingAroundAdvice implements MethodInterceptor { ... }
最后看看客户端吧:
public class Client { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml"); // 获取 Spring Context Greeting greeting = (Greeting) context.getBean("greetingProxy"); // 从 Context 中根据 id 获取 Bean 对象(其实就是一个代理) greeting.sayHello("Jack"); // 调用代理的方法 } }
代码量确实少了,咱们将配置性的代码放入配置文件,这样也有助于后期维护。更重要的是,代码只关注于业务逻辑,而将配置放入文件中。这是一条最佳实践!
除了上面提到的那三类加强之外,其实还有两类加强也须要了解一下,关键的时候您要能想获得它们才行。
7. Spring AOP:抛出加强
程序报错,抛出异常了,通常的作法是打印到控制台或日志文件中,这样不少地方都得去处理,有没有一个一劳永逸的方法呢?那就是 Throws Advice(抛出加强),它确实很强,不信你就继续往下看:
@Component public class GreetingImpl implements Greeting { @Override public void sayHello(String name) { System.out.println("Hello! " + name); throw new RuntimeException("Error"); // 故意抛出一个异常,看看异常信息可否被拦截到 } }
下面是抛出加强类的代码:
@Component public class GreetingThrowAdvice implements ThrowsAdvice { public void afterThrowing(Method method, Object[] args, Object target, Exception e) { System.out.println("---------- Throw Exception ----------"); System.out.println("Target Class: " + target.getClass().getName()); System.out.println("Method Name: " + method.getName()); System.out.println("Exception Message: " + e.getMessage()); System.out.println("-------------------------------------"); } }
抛出加强类须要实现 org.springframework.aop.ThrowsAdvice 接口,在接口方法中可获取方法、参数、目标对象、异常对象等信息。咱们能够把这些信息统一写入到日志中,固然也能够持久化到数据库中。
这个功能确实太棒了!但还有一个更厉害的加强。若是某个类实现了 A 接口,但没有实现 B 接口,那么该类能够调用 B 接口的方法吗?若是您没有看到下面的内容,必定不敢相信原来这是可行的!
8. Spring AOP:引入加强
以上提到的都是对方法的加强,那可否对类进行加强呢?用 AOP 的行话来说,对方法的加强叫作 Weaving(织入),而对类的加强叫作 Introduction(引入)。而 Introduction Advice(引入加强)就是对类的功能加强,它也是 Spring AOP 提供的最后一种加强。建议您一开始千万不要去看《Spring Reference》,不然您必定会后悔的。由于当您看了如下的代码示例后,必定会完全明白什么才是引入加强。
定义了一个新接口 Apology(道歉):
public interface Apology { void saySorry(String name); }
但我不想在代码中让 GreetingImpl 直接去实现这个接口,我想在程序运行的时候动态地实现它。由于假如我实现了这个接口,那么我就必定要改写 GreetingImpl 这个类,关键是我不想改它,或许在真实场景中,这个类有1万行代码,我实在是不敢动了。因而,我须要借助 Spring 的引入加强。这个有点意思了!
@Component public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology { @Override public Object invoke(MethodInvocation invocation) throws Throwable { return super.invoke(invocation); } @Override public void saySorry(String name) { System.out.println("Sorry! " + name); } }
以上定义了一个引入加强类,扩展了 org.springframework.aop.support.DelegatingIntroductionInterceptor 类,同时也实现了新定义的 Apology 接口。在类中首先覆盖了父类的 invoke() 方法,而后实现了 Apology 接口的方法。我就是想用这个加强类去丰富 GreetingImpl 类的功能,那么这个 GreetingImpl 类无需直接实现 Apology 接口,就能够在程序运行的时候调用 Apology 接口的方法了。这简直是太神奇的!
看看是如何配置的吧:
<?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" 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.xsd"> <context:component-scan base-package="aop.demo"/> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="aop.demo.Apology"/> <!-- 须要动态实现的接口 --> <property name="target" ref="greetingImpl"/> <!-- 目标类 --> <property name="interceptorNames" value="greetingIntroAdvice"/> <!-- 引入加强 --> <property name="proxyTargetClass" value="true"/> <!-- 代理目标类(默认为 false,代理接口) --> </bean> </beans>
须要注意 proxyTargetClass 属性,它代表是否代理目标类,默认为 false,也就是代理接口了,此时 Spring 就用 JDK 动态代理。若是为 true,那么 Spring 就用 CGLib 动态代理。这简直就是太方便了!Spring 封装了这一切,让程序员不在关心那么多的细节。咱们要向老罗同志致敬,您是咱们心中永远的 idol!
当您看完下面的客户端代码,必定会彻底明白以上的这一切:
public class Client { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml"); GreetingImpl greetingImpl = (GreetingImpl) context.getBean("greetingProxy"); // 注意:转型为目标类,而并不是它的 Greeting 接口 greetingImpl.sayHello("Jack"); Apology apology = (Apology) greetingImpl; // 将目标类强制向上转型为 Apology 接口(这是引入加强给咱们带来的特性,也就是“接口动态实现”功能) apology.saySorry("Jack"); } }
没想到 saySorry() 方法原来是能够被 greetingImpl 对象来直接调用的,只需将其强制转换为该接口便可。
从写死代码,到使用代理;从编程式 Spring AOP 到声明式 Spring AOP。一切都朝着简单实用主义的方向在发展。沿着 Spring AOP 的方向,Rod Johnson(老罗)花了很多心思,都是为了让咱们使用 Spring 框架时不会感觉到麻烦,但事实却并不是如此。那么,后来老罗究竟对 Spring AOP 作了哪些改进呢?
9. Spring AOP:切面
以前谈到的 AOP 框架其实能够将它理解为一个拦截器框架,但这个拦截器彷佛很是武断。好比说,若是它拦截了一个类,那么它就拦截了这个类中全部的方法。相似地,当咱们在使用动态代理的时候,其实也遇到了这个问题。须要在代码中对所拦截的方法名加以判断,才能过滤出咱们须要拦截的方法,想一想这种作法确实不太优雅。在大量的真实项目中,彷佛咱们只须要拦截特定的方法就好了,不必拦截全部的方法。因而,老罗同志借助了 AOP 的一个很重要的工具,Advisor(切面),来解决这个问题。它也是 AOP 中的核心!是咱们关注的重点!
也就是说,咱们能够经过切面,将加强类与拦截匹配条件组合在一块儿,而后将这个切面配置到 ProxyFactory 中,从而生成代理。
这里提到这个“拦截匹配条件”在 AOP 中就叫作 Pointcut(切点),其实说白了就是一个基于表达式的拦截条件罢了。
概括一下,Advisor(切面)封装了 Advice(加强)与 Pointcut(切点 )。当您理解了这句话后,就往下看吧。
我在 GreetingImpl 类中故意增长了两个方法,都以“good”开头。下面要作的就是拦截这两个新增的方法,而对 sayHello() 方法不做拦截。
@Component public class GreetingImpl implements Greeting { @Override public void sayHello(String name) { System.out.println("Hello! " + name); } public void goodMorning(String name) { System.out.println("Good Morning! " + name); } public void goodNight(String name) { System.out.println("Good Night! " + name); } }
在 Spring AOP 中,老罗已经给咱们提供了许多切面类了,这些切面类我我的感受最好用的就是基于正则表达式的切面类。看看您就明白了:
<?xml version="1.0" encoding="UTF-8"?> <beans ..."> <context:component-scan base-package="aop.demo"/> <!-- 配置一个切面 --> <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice" ref="greetingAroundAdvice"/> <!-- 加强 --> <property name="pattern" value="aop.demo.GreetingImpl.good.*"/> <!-- 切点(正则表达式) --> </bean> <!-- 配置一个代理 --> <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="greetingImpl"/> <!-- 目标类 --> <property name="interceptorNames" value="greetingAdvisor"/> <!-- 切面 --> <property name="proxyTargetClass" value="true"/> <!-- 代理目标类 --> </bean> </beans>
注意以上代理对象的配置中的 interceptorNames,它再也不是一个加强,而是一个切面,由于已经将加强封装到该切面中了。此外,切面还定义了一个切点(正则表达式),其目的是为了只将知足切点匹配条件的方法进行拦截。
须要强调的是,这里的切点表达式是基于正则表达式的。示例中的“aop.demo.GreetingImpl.good.*”表达式后面的“.*”表示匹配全部字符,翻译过来就是“匹配 aop.demo.GreetingImpl 类中以 good 开头的方法”。
除了 RegexpMethodPointcutAdvisor 之外,在 Spring AOP 中还提供了几个切面类,好比:
总的来讲,让用户去配置一个或少数几个代理,彷佛还能够接受,但随着项目的扩大,代理配置就会愈来愈多,配置的重复劳动就多了,麻烦不说,还很容易出错。可否让 Spring 框架为咱们自动生成代理呢?
10. Spring AOP:自动代理(扫描 Bean 名称)
Spring AOP 提供了一个可根据 Bean 名称来自动生成代理的工具,它就是 BeanNameAutoProxyCreator。是这样配置的:
<?xml version="1.0" encoding="UTF-8"?> <beans ...> ... <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> <property name="beanNames" value="*Impl"/> <!-- 只为后缀是“Impl”的 Bean 生成代理 --> <property name="interceptorNames" value="greetingAroundAdvice"/> <!-- 加强 --> <property name="optimize" value="true"/> <!-- 是否对代理生成策略进行优化 --> </bean> </beans>
以上使用 BeanNameAutoProxyCreator 只为后缀为“Impl”的 Bean 生成代理。须要注意的是,这个地方咱们不能定义代理接口,也就是 interfaces 属性,由于咱们根本就不知道这些 Bean 到底实现了多少接口。此时不能代理接口,而只能代理类。因此这里提供了一个新的配置项,它就是 optimize。若为 true 时,则可对代理生成策略进行优化(默认是 false 的)。也就是说,若是该类有接口,就代理接口(使用 JDK 动态代理);若是没有接口,就代理类(使用 CGLib 动态代理)。而并不是像以前使用的 proxyTargetClass 属性那样,强制代理类,而不考虑代理接口的方式。可见 Spring AOP 确实为咱们提供了不少很好地服务!
既然 CGLib 能够代理任何的类了,那为何还要用 JDK 的动态代理呢?确定您会这样问。
根据多年来实际项目经验得知:CGLib 建立代理的速度比较慢,但建立代理后运行的速度却很是快,而 JDK 动态代理正好相反。若是在运行的时候不断地用 CGLib 去建立代理,系统的性能会大打折扣,因此建议通常在系统初始化的时候用 CGLib 去建立代理,并放入 Spring 的 ApplicationContext 中以备后用。
以上这个例子只能匹配目标类,而不能进一步匹配其中指定的方法,要匹配方法,就要考虑使用切面与切点了。Spring AOP 基于切面也提供了一个自动代理生成器:DefaultAdvisorAutoProxyCreator。
11. Spring AOP:自动代理(扫描切面配置)
为了匹配目标类中的指定方法,咱们仍然须要在 Spring 中配置切面与切点:
<?xml version="1.0" encoding="UTF-8"?> <beans ...> ... <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="pattern" value="aop.demo.GreetingImpl.good.*"/> <property name="advice" ref="greetingAroundAdvice"/> </bean> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> <property name="optimize" value="true"/> </bean> </beans>
这里无需再配置代理了,由于代理将会由 DefaultAdvisorAutoProxyCreator 自动生成。也就是说,这个类能够扫描全部的切面类,并为其自动生成代理。
看来无论怎样简化,老罗始终解决不了切面的配置,这件繁重的手工劳动。在 Spring 配置文件中,仍然会存在大量的切面配置。然而在有不少状况下 Spring AOP 所提供的切面类真的不太够用了,好比:想拦截指定注解的方法,咱们就必须扩展 DefaultPointcutAdvisor 类,自定义一个切面类,而后在 Spring 配置文件中进行切面配置。不作不知道,作了您就知道至关麻烦了。
老罗的解决方案彷佛已经掉进了切面类的深渊,这还真是所谓的“面向切面编程”了,最重要的是切面,最麻烦的也是切面。
必需要把切面配置给简化掉,Spring 才能有所突破!
神同样的老罗总算认识到了这一点,接受了网友们的建议,集成了 AspectJ,同时也保留了以上提到的切面与代理配置方式(为了兼容老的项目,更为了维护本身的面子)。将 Spring 与 AspectJ 集成与直接使用 AspectJ 是不一样的,咱们不须要定义 AspectJ 类(它是扩展了 Java 语法的一种新的语言,还须要特定的编译器),只须要使用 AspectJ 切点表达式便可(它是比正则表达式更加友好的表现形式)。
12. Spring + AspectJ(基于注解:经过 AspectJ execution 表达式拦截方法)
下面以一个最简单的例子,实现以前提到的环绕加强。先定义一个 Aspect 切面类:
@Aspect @Component public class GreetingAspect { @Around("execution(* aop.demo.GreetingImpl.*(..))") public Object around(ProceedingJoinPoint pjp) throws Throwable { before(); Object result = pjp.proceed(); after(); return result; } private void before() { System.out.println("Before"); } private void after() { System.out.println("After"); } }
注意:类上面标注的 @Aspect 注解,这代表该类是一个 Aspect(其实就是 Advisor)。该类无需实现任何的接口,只需定义一个方法(方法叫什么名字都无所谓),只需在方法上标注 @Around 注解,在注解中使用了 AspectJ 切点表达式。方法的参数中包括一个 ProceedingJoinPoint 对象,它在 AOP 中称为 Joinpoint(链接点),能够经过该对象获取方法的任何信息,例如:方法名、参数等。
下面重点来分析一下这个切点表达式:
execution(* aop.demo.GreetingImpl.*(..))
是否是比正则表达式的可读性更强呢?若是想匹配指定的方法,只需将第二个“*”改成指定的方法名称便可。
如何配置呢?看看是有多简单吧:
<?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.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <context:component-scan base-package="aop.demo"/> <aop:aspectj-autoproxy proxy-target-class="true"/> </beans>
两行配置就好了,不须要配置大量的代理,更不须要配置大量的切面,真是太棒了!须要注意的是 proxy-target-class=”true” 属性,它的默认值是 false,默认只能代理接口(使用 JDK 动态代理),当为 true 时,才能代理目标类(使用 CGLib 动态代理)。
Spring 与 AspectJ 结合的威力远远不止这些,咱们来点时尚的吧,拦截指定注解的方法怎么样?
13. Spring + AspectJ(基于注解:经过 AspectJ @annotation 表达式拦截方法)
为了拦截指定的注解的方法,咱们首先须要来自定义一个注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Tag { }
以上定义了一个 @Tag 注解,此注解可标注在方法上,在运行时生效。
只需将前面的 Aspect 类的切点表达式稍做改动:
@Aspect @Component public class GreetingAspect { @Around("@annotation(aop.demo.Tag)") public Object around(ProceedingJoinPoint pjp) throws Throwable { ... } ... }
此次使用了 @annotation() 表达式,只需在括号内定义须要拦截的注解名称便可。
直接将 @Tag 注解定义在您想要拦截的方法上,就这么简单:
@Component public class GreetingImpl implements Greeting { @Tag @Override public void sayHello(String name) { System.out.println("Hello! " + name); } }
以上示例中只有一个方法,若是有多个方法,咱们只想拦截其中某些时,这种解决方案会更加有价值。
除了 @Around 注解外,其实还有几个相关的注解,稍微概括一下吧:
此外还有一个 @AfterReturning(返回后加强),也可理解为 Finally 加强,至关于 finally 语句,它是在方法结束后执行的,也就说说,它比 @After 还要晚一些。
最后一个 @DeclareParents 居然就是引入加强!为何不叫作 @Introduction 呢?我也不知道为何,但它干的活就是引入加强。
14. Spring + AspectJ(引入加强)
为了实现基于 AspectJ 的引入加强,咱们一样须要定义一个 Aspect 类:
@Aspect @Component public class GreetingAspect { @DeclareParents(value = "aop.demo.GreetingImpl", defaultImpl = ApologyImpl.class) private Apology apology; }
只须要在 Aspect 类中定义一个须要引入加强的接口,它也就是运行时须要动态实现的接口。在这个接口上标注了 @DeclareParents 注解,该注解有两个属性:
咱们只须要对引入的接口提供一个默认实现类便可完成引入加强:
public class ApologyImpl implements Apology { @Override public void saySorry(String name) { System.out.println("Sorry! " + name); } }
以上这个实现会在运行时自动加强到 GreetingImpl 类中,也就是说,无需修改 GreetingImpl 类的代码,让它去实现 Apology 接口,咱们单独为该接口提供一个实现类(ApologyImpl),来作 GreetingImpl 想作的事情。
仍是用一个客户端来尝试一下吧:
public class Client { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("aop/demo/spring.xml"); Greeting greeting = (Greeting) context.getBean("greetingImpl"); greeting.sayHello("Jack"); Apology apology = (Apology) greeting; // 强制转型为 Apology 接口 apology.saySorry("Jack"); } }
从 Spring ApplicationContext 中获取 greetingImpl 对象(实际上是个代理对象),可转型为本身静态实现的接口 Greeting,也可转型为本身动态实现的接口 Apology,切换起来很是方便。
使用 AspectJ 的引入加强比原来的 Spring AOP 的引入加强更加方便了,并且还可面向接口编程(之前只能面向实现类),这也算一个很是巨大的突破。
这一切真的已经很是强大也很是灵活了!但仍然仍是有用户不能尝试这些特性,由于他们还在使用 JDK 1.4(根本就没有注解这个东西),怎么办呢?没想到 Spring AOP 为那些遗留系统也考虑到了。
15. Spring + AspectJ(基于配置)
除了使用 @Aspect 注解来定义切面类之外,Spring AOP 也提供了基于配置的方式来定义切面类:
<?xml version="1.0" encoding="UTF-8"?> <beans ..."> <bean id="greetingImpl" class="aop.demo.GreetingImpl"/> <bean id="greetingAspect" class="aop.demo.GreetingAspect"/> <aop:config> <aop:aspect ref="greetingAspect"> <aop:around method="around" pointcut="execution(* aop.demo.GreetingImpl.*(..))"/> </aop:aspect> </aop:config> </beans>
使用 <aop:config> 元素来进行 AOP 配置,在其子元素中配置切面,包括加强类型、目标方法、切点等信息。
不管您是不能使用注解,仍是不肯意使用注解,Spring AOP 都能为您提供全方位的服务。
好了,我所知道的比较实用的 AOP 技术都在这里了,固然还有一些更为高级的特性,因为我的精力有限,这里就再也不深刻了。
仍是依照惯例,给一张牛逼的高清无码思惟导图,总结一下以上各个知识点:
再来一张表格,总结一下各种加强类型所对应的解决方案:
加强类型 | 基于 AOP 接口 | 基于 @Aspect | 基于 <aop:config> |
Before Advice(前置加强) | MethodBeforeAdvice | @Before | <aop:before> |
AfterAdvice(后置加强) | AfterReturningAdvice | @After | <aop:after> |
AroundAdvice(环绕加强) | MethodInterceptor | @Around | <aop:around> |
ThrowsAdvice(抛出加强 | ThrowsAdvice | @AfterThrowing | <aop:after-throwing> |
IntroductionAdvice(引入加强) | DelegatingIntroductionInterceptor | @DeclareParents | <aop:declare-parents> |
最后给一张 UML 类图描述一下 Spring AOP 的总体架构: