又是一个周末,刚给宝宝喂完牛奶,终于让她睡着了。所以现在我才能腾出手来,坐在电脑面前给大家写这篇文章。
今天我要和大家分享的是 AOP(Aspect-Oriented Programming)这个东西,名字与 OOP 仅差一个字母,其实它是对 OOP 编程方式的一种补充,并非是取而代之。翻译过来就是“面向方面编程”,可我更倾向于翻译为“面向切面编程”。它听起有些的神秘,为什么呢?当你看完这篇文章的时候,就就知道,我们做的很重要的工作就是去写这个“切面” 。那么什么是“切面”呢?
没错!就是用一把刀来切一坨面。注意,相对于面而言,我们一定是横着来切它,这简称为“横切”。可以把一段代码想象成一坨面,同样也可以用一把刀来横切它,下面要做的就是如何去实现这把刀!
需要澄清的是,这个概念不是由 Rod Johnson(老罗)提出的。其实很早以前就有了,目前最知名最强大的 Java 开源项目就是 AspectJ 了,然而它的前身是 AspectWerkz(该项目已经在 2005 年停止更新),这才是 AOP 的老祖宗。老罗(一个头发秃得和我老爸有一拼的天才)写了一个叫做 Spring 框架,从此一炮走红,成为了 Spring 之父。他在自己的 IOC 的基础之上,又实现了一套 AOP 的框架,后来仿佛发现自己越来越走进深渊里,在不能自拔的时候,有人建议他还是集成 AspectJ 吧,他在万般无奈之下才接受了该建议。于是,我们现在用得最多的想必就是 Spring + AspectJ 这种 AOP 框架了。
那么 AOP 到底是什么?如何去使用它?本文将逐步带您进入 AOP 的世界,让您感受到前所未有的畅快!
不过在开始讲解 AOP 之前,我想有必要回忆一下这段代码:
1. 写死代码
先来一个接口:
1 |
public interface Greeting {
|
3 |
void sayHello(String name);
|
还有一个实现类:
01 |
public class GreetingImpl implements Greeting {
|
04 |
public void sayHello(String name) {
|
06 |
System.out.println( "Hello! " + name);
|
10 |
private void before() {
|
11 |
System.out.println( "Before" );
|
14 |
private void after() {
|
15 |
System.out.println( "After" );
|
before() 与 after() 方法写死在 sayHello() 方法体中了,这样的代码的味道非常不好。如果哪位仁兄大量写了这样的代码,肯定要被你的架构师骂个够呛。
比如:我们要统计每个方法的执行时间,以对性能作出评估,那是不是要在每个方法的一头一尾都做点手脚呢?
再比如:我们要写一个 JDBC 程序,那是不是也要在方法的开头去连接数据库,方法的末尾去关闭数据库连接呢?
这样的代码只会把程序员累死,把架构师气死!
一定要想办法对上面的代码进行重构,首先给出三个解决方案:
2. 静态代理
最简单的解决方案就是使用静态代理模式了,我们单独为 GreetingImpl 这个类写一个代理类:
01 |
public class GreetingProxy implements Greeting {
|
03 |
private GreetingImpl greetingImpl;
|
05 |
public GreetingProxy(GreetingImpl greetingImpl) {
|
06 |
this .greetingImpl = greetingImpl;
|
10 |
public void sayHello(String name) {
|
12 |
greetingImpl.sayHello(name);
|
16 |
private void before() {
|
17 |
System.out.println( "Before" );
|
20 |
private void after() {
|
21 |
System.out.println( "After" );
|
就用这个 GreetingProxy 去代理 GreetingImpl,下面看看客户端如何来调用:
3 |
public static void main(String[] args) {
|
4 |
Greeting greetingProxy = new GreetingProxy( new GreetingImpl());
|
5 |
greetingProxy.sayHello( "Jack" );
|
这样写没错,但是有个问题,XxxProxy 这样的类会越来越多,如何才能将这些代理类尽可能减少呢?最好只有一个代理类。
这时我们就需要使用 JDK 提供的动态代理了。
3. JDK 动态代理
01 |
public class JDKDynamicProxy implements InvocationHandler {
|
03 |
private Object target;
|
05 |
public JDKDynamicProxy(Object target) {
|
09 |
@SuppressWarnings ( "unchecked" )
|
10 |
public <T> T getProxy() {
|
11 |
return (T) Proxy.newProxyInstance(
|
12 |
target.getClass().getClassLoader(),
|
13 |
target.getClass().getInterfaces(),
|
19 |
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
21 |
Object result = method.invoke(target, args);
|
26 |
private void before() {
|
27 |
System.out.println( "Before" );
|
30 |
private void after() {
|
31 |
System.out.println( "After" );
|
客户端是这样调用的:
3 |
public static void main(String[] args) {
|
4 |
Greeting greeting = new JDKDynamicProxy( new GreetingImpl()).getProxy();
|
5 |
greeting.sayHello( "Jack" );
|
这样所有的代理类都合并到动态代理类中了,但这样做仍然存在一个问题:JDK 给我们提供的动态代理只能代理接口,而不能代理没有接口的类。有什么方法可以解决呢?
4. CGLib 动态代理
我们使用开源的 CGLib 类库可以代理没有接口的类,这样就弥补了 JDK 的不足。CGLib 动态代理类是这样玩的:
01 |
public class CGLibDynamicProxy implements MethodInterceptor {
|
03 |
private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
|
05 |
private CGLibDynamicProxy() {
|
08 |
public static CGLibDynamicProxy getInstance() {
|
12 |
@SuppressWarnings ( "unchecked" )
|
13 |
public <T> T getProxy(Class<T> cls) {
|
14 |
return (T) Enhancer.create(cls, this );
|
18 |
public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
|
20 |
Object result = proxy.invokeSuper(target, args);
|
25 |
private void before() {
|
26 |
System.out.println( "Before" );
|
29 |
private void after() {
|
30 |
System.out.println( "After" );
|
以上代码中了 Singleton 模式,那么客户端调用也更加轻松了:
3 |
public static void main(String[] args) {
|
4 |
Greeting greeting = CGLibDynamicProxy.getInstance().getProxy(GreetingImpl. class );
|
5 |
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(环绕增强),就像汉堡一样,中间夹一根火腿。
这三个概念是不是轻松地理解了呢?如果是,那就继续吧!
我们下面要做的就是去实现这些所谓的“增强类”,让他们横切到代码中,而不是将这些写死在代码中。
先来一个前置增强类吧:
1 |
public class GreetingBeforeAdvice implements MethodBeforeAdvice {
|
4 |
public void before(Method method, Object[] args, Object target) throws Throwable {
|
5 |
System.out.println( "Before" );
|
注意:这个类实现了 org.springframework.aop.MethodBeforeAdvice 接口,我们将需要增强的代码放入其中。
再来一个后置增强类吧:
1 |
public class GreetingAfterAdvice implements AfterReturningAdvice {
|
4 |
public void afterReturning(Object result, Method method, Object[] args, Object target) throws Throwable {
|
5 |
System.out.println( "After" );
|
类似地,这个类实现了 org.springframework.aop.AfterReturningAdvice 接口。
最后用一个客户端来把它们集成起来,看看如何调用吧:
03 |
public static void main(String[] args) {
|
04 |
ProxyFactory proxyFactory = new ProxyFactory();
|
05 |
proxyFactory.setTarget( new GreetingImpl());
|
06 |
proxyFactory.addAdvice( new GreetingBeforeAdvice());
|
07 |
proxyFactory.addAdvice( new GreetingAfterAdvice());
|
09 |
Greeting greeting = (Greeting) proxyFactory.getProxy();
|
10 |
greeting.sayHello( "Jack" );
|
请仔细阅读以上代码及其注释,您会发现,其实 Spring AOP 还是挺简单的,对吗?
当然,我们完全可以只定义一个增强类,让它同时实现 MethodBeforeAdvice 与 AfterReturningAdvice 这两个接口,如下:
01 |
public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice, AfterReturningAdvice {
|
04 |
public void before(Method method, Object[] args, Object target) throws Throwable {
|
05 |
System.out.println( "Before" );
|
09 |
public void afterReturning(Object result, Method method, Object[] args, Object target) throws Throwable {
|
10 |
System.out.println( "After" );
|
这样我们只需要使用一行代码,同时就可以添加前置与后置增强:
1 |
proxyFactory.addAdvice( new GreetingBeforeAndAfterAdvice());
|
刚才有提到“环绕增强”,其实这个东西可以把“前置增强”与“后置增强”的功能给合并起来,无需让我们同时实现以上两个接口。
01 |
public class GreetingAroundAdvice implements MethodInterceptor {
|
04 |
public Object invoke(MethodInvocation invocation) throws Throwable {
|
06 |
Object result = invocation.proceed();
|
11 |
private void before() {
|
12 |
System.out.println( "Before" );
|
15 |
private void after() {
|
16 |
System.out.println( "After" );
|
环绕增强类需要实现 org.aopalliance.intercept.MethodInterceptor 接口。注意,这个接口不是 Spring 提供的,它是 AOP 联盟(一个很牛逼的联盟)写的,Spring 只是借用了它。
在客户端中同样也需要将该增强类的对象添加到代理工厂中:
1 |
proxyFactory.addAdvice( new GreetingAroundAdvice());
|
好了,这就是 Spring AOP 的基本用法,但这只是“编程式”而已。Spring AOP 如果只是这样,那就太傻逼了,它曾经也是一度宣传用 Spring 配置文件的方式来定义 Bean 对象,把代码中的 new 操作全部解脱出来。
6. Spring AOP:前置增强、后置增强、环绕增强(声明式)
先看 Spring 配置文件是如何写的吧:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?>
|
02 |
< beans xmlns = "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">
|
11 |
< context:component-scan base-package = "aop.demo" />
|
14 |
< bean id = "greetingProxy" class = "org.springframework.aop.framework.ProxyFactoryBean" >
|
15 |
< property name = "interfaces" value = "aop.Greeting" />
|
16 |
< property name = "target" ref = "greetingImpl" />
|
17 |
< property name = "interceptorNames" >
|
19 |
< value >greetingAroundAdvice</ value >
|
一定要阅读以上代码的注释,其实使用 ProxyFactoryBean 就可以取代前面的 ProxyFactory,其实它们俩就一回事儿。我认为 interceptorNames 应该改名为 adviceNames 或许会更容易让人理解,不就是往这个属性里面添加增强类吗?
此外,如果只有一个增强类,可以使用以下方法来简化:
3 |
< bean id = "greetingProxy" class = "org.springframework.aop.framework.ProxyFactoryBean" >
|
4 |
< property name = "interfaces" value = "aop.Greeting" />
|
5 |
< property name = "target" ref = "greetingImpl" />
|
6 |
< property name = "interceptorNames" value = "greetingAroundAdvice" />
|
还需要注意的是,这里使用了 Spring 2.5+ 的特性“Bean 扫描”,这样我们就无需在 Spring 配置文件里不断地定义 <bean id="xxx" class="xxx"/> 了,从而解脱了我们的双手。
看看这是有多么的简单:
2 |
public class GreetingImpl implements Greeting {
|
2 |
public class GreetingAroundAdvice implements MethodInterceptor {
|
最后看看客户端吧:
3 |
public static void main(String[] args) {
|
4 |
ApplicationContext context = new ClassPathXmlApplicationContext( "aop/demo/spring.xml" );
|
5 |
Greeting greeting = (Greeting) context.getBean( "greetingProxy" );
|
6 |
greeting.sayHello( "Jack" );
|
代码量确实少了,我们将配置性的代码放入配置文件,这样也有助于后期维护。更重要的是,代码只关注于业务逻辑,而将配置放入文件中。这是一条最佳实践!
除了上面提到的那三类增强以外,其实还有两类增强也需要了解一下,关键的时候您要能想得到它们才行。
7. Spring AOP:抛出增强
程序报错,抛出异常了,一般的做法是打印到控制台或日志文件中,这样很多地方都得去处理,有没有一个一劳永逸的方法呢?那就是 Throws Advice(抛出增强),它确实很强,不信你就继续往下看:
02 |
public class GreetingImpl implements Greeting {
|
05 |
public void sayHello(String name) {
|
06 |
System.out.println( "Hello! " + name);
|
08 |
throw new RuntimeException( "Error" );
|
下面是抛出增强类的代码:
02 |
public class GreetingThrowAdvice implements ThrowsAdvice {
|
04 |
public void afterThrowing(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( "-------------------------------------" );
|
抛出增强类需要实现 org.springframework.aop.ThrowsAdvice 接口,在接口方法中可获取方法、参数、目标对象、异常对象等信息。我们可以把这些信息统一写入到日志中,当然也可以持久化到数据库中。
这个功能确实太棒了!但还有一个更厉害的增强。如果某个类实现了 A 接口,但没有实现 B 接口,那么该类可以调用 B 接口的方法吗?如果您没有看到下面的内容,一定不敢相信原来这是可行的!
8. Spring AOP:引入增强
以上提到的都是对方法的增强,那能否对类进行增强呢?用 AOP 的行话来讲,对方法的增强叫做 Weaving(织入),而对类的增强叫做 Introduction(引入)。而 Introduction Advice(引入增强)就是对类的功能增强,它也是 Spring AOP 提供的最后一种增强。建议您一开始千万不要去看《Spring Reference》,否则您一定会后悔的。因为当您看了以下的代码示例后,一定会彻底明白什么才是引入增强。
定义了一个新接口 Apology(道歉):
1 |
public interface Apology {
|
3 |
void saySorry(String name);
|
但我不想在代码中让 GreetingImpl 直接去实现这个接口,我想在程序运行的时候动态地实现它。因为假如我实现了这个接口,那么我就一定要改写 GreetingImpl 这个类,关键是我不想改它,或许在真实场景中,这个类有1万行代码,我实在是不敢动了。于是,我需要借助 Spring 的引入增强。这个有点意思了!
02 |
public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology {
|
05 |
public Object invoke(MethodInvocation invocation) throws Throwable {
|
06 |
return super .invoke(invocation);
|
10 |
public void saySorry(String name) {
|
11 |
System.out.println( "Sorry! " + name);
|
以上定义了一个引入增强类,扩展 了 org.springframework.aop.support.DelegatingIntroductionInterceptor 类,同时也实现了新定义的 Apology 接口。在类中首先覆盖了父类的 invoke() 方法,然后实现了 Apology 接口的方法。我就是想用这个增强类去丰富 GreetingImpl 类的功能,那么这个 GreetingImpl 类无需直接实现 Apology 接口,就可以在程序运行的时候调用 Apology 接口的方法了。这简直是太神奇的!
看看是如何配置的吧:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?>
|
02 |
< beans xmlns = "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">
|
10 |
< context:component-scan base-package = "aop.demo" />
|
12 |
< bean id = "greetingProxy" class = "org.springframework.aop.framework.ProxyFactoryBean" >
|
13 |
< property name = "interfaces" value = "aop.demo.Apology" />
|
14 |
< property name = "target" ref = "greetingImpl" />
|
15 |
< property name = "interceptorNames" value = "greetingIntroAdvice" />
|
16 |
< property name = "proxyTargetClass" value = "true" />
|
需要注意 proxyTargetClass 属性,它表明是否代理目标类,默认为 false,也就是代理接口了,此时 Spring 就用 JDK 动态代理。如果为 false,那么 Spring 就用 CGLib 动态代理。这简直就是太方便了!Spring 封装了这一切,让程序员不在关心那么多的细节。我们要向老罗同志致敬,您是我们心中永远的 idol!
当您看完下面的客户端代码,一定会完全明白以上的这一切:
03 |
public static void main(String[] args) {
|
04 |
ApplicationContext context = new ClassPathXmlApplicationContext( "aop/demo/spring.xml" );
|
05 |
GreetingImpl greetingImpl = (GreetingImpl) context.getBean( "greetingProxy" );
|
06 |
greetingImpl.sayHello( "Jack" );
|
08 |
Apology apology = (Apology) greetingImpl;
|
09 |
apology.saySorry( "Jack" );
|
没想到 saySorry() 方法原来是可以被 greetingImpl 对象来直接调用的,只需将其强制转换为该接口即可。
我们再次感谢 Spring AOP,感谢老罗给我们提供了这么强大的特性!
其实,Spring AOP 还有很多精彩的地方,下一篇将介绍更多更有价值的 AOP 技术,让大家得到更多的收获。
未完,待续...
AOP 那点儿事(续集)
在上篇中,我们从写死代码,到使用代理;从编程式 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() 方法不作拦截。
02 |
public class GreetingImpl implements Greeting {
|
05 |
public void sayHello(String name) {
|
06 |
System.out.println( "Hello! " + name);
|
09 |
public void goodMorning(String name) {
|
10 |
System.out.println( "Good Morning! " + name);
|
13 |
public void goodNight(String name) {
|
14 |
System.out.println( "Good Night! " + name);
|
在 Spring AOP 中,老罗已经给我们提供了许多切面类了,这些切面类我个人感觉最好用的就是基于正则表达式的切面类。看看您就明白了:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?>
|
04 |
< context:component-scan base-package = "aop.demo" />
|
07 |
< bean id = "greetingAdvisor" class = "org.springframework.aop.support.RegexpMethodPointcutAdvisor" >
|
08 |
< property name = "advice" ref = "greetingAroundAdvice" />
|
09 |
< property name = "pattern" value = "aop.demo.GreetingImpl.good.*" />
|
13 |
< bean id = "greetingProxy" class = "org.springframework.aop.framework.ProxyFactoryBean" >
|
14 |
< property name = "target" ref = "greetingImpl" />
|
15 |
< property name = "interceptorNames" value = "greetingAdvisor" />
|
16 |
< property name = "proxyTargetClass" value = "true" />
|
注意以上代理对象的配置中的 interceptorNames,它不再是一个增强,而是一个切面,因为已经将增强封装到该切面中了。此外,切面还定义了一个切点(正则表达式),其目的是为了只将满足切点匹配条件的方法进行拦截。
需要强调的是,这里的切点表达式是基于正则表达式的。示例中的“aop.demo.GreetingImpl.good.*”表达式后面的“.*”表示匹配所有字符,翻译过来就是“匹配 aop.demo.GreetingImpl 类中以 good 开头的方法”。
除了 RegexpMethodPointcutAdvisor 以外,在 Spring AOP 中还提供了几个切面类,比如:
- DefaultPointcutAdvisor:默认切面(可扩展它来自定义切面)
- NameMatchMethodPointcutAdvisor:根据方法名称进行匹配的切面
- StaticMethodMatcherPointcutAdvisor:用于匹配静态方法的切面
总的来说,让用户去配置一个或少数几个代理,似乎还可以接受,但随着项目的扩大,代理配置就会越来越多,配置的重复劳动就多了,麻烦不说,还很容易出错。能否让 Spring 框架为我们自动生成代理呢?
10. Spring AOP:自动代理(扫描 Bean 名称)
Spring AOP 提供了一个可根据 Bean 名称来自动生成代理的工具,它就是 BeanNameAutoProxyCreator。是这样配置的:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?>
|
06 |
< bean class = "org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" >
|
07 |
< property name = "beanNames" value = "*Impl" />
|
08 |
< property name = "interceptorNames" value = "greetingAroundAdvice" />
|
09 |
< property name = "optimize" value = "true" />
|
以上使用 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 中配置切面与切点:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?>
|
06 |
< bean id = "greetingAdvisor" class = "org.springframework.aop.support.RegexpMethodPointcutAdvisor" >
|
07 |
< property name = "pattern" value = "aop.demo.GreetingImpl.good.*" />
|
08 |
< property name = "advice" ref = "greetingAroundAdvice" />
|
11 |
< bean class = "org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" >
|
12 |
< property name = "optimize" value = "true" />
|
这里无需再配置代理了,因为代理将会由 DefaultAdvisorAutoProxyCreator 自动生成。也就是说,这个类可以扫描所有的切面类,并为其自动生成代理。
看来不管怎样简化,老罗始终解决不了切面的配置,这件繁重的手工劳动。在 Spring 配置文件中,仍然会存在大量的切面配置。然而在有很多情况下 Spring AOP 所提供的切面类真的不太够用了,比如:想拦截指定注解的方法,我们就必须扩展 DefaultPointcutAdvisor 类,自定义一个切面类,然后在 Spring 配置文件中进行切面配置。不做不知道,做了您就知道相当麻烦了。
老罗的解决方案似乎已经掉进了切面类的深渊,这还真是所谓的“面向切面编程”了,最重要的是切面,最麻烦的也是切面。
必须要把切面配置给简化掉,Spring 才能有所突破!
神一样的老罗总算认识到了这一点,接受了网友们的建议,集成了 AspectJ,同时也保留了以上提到的切面与代理配置方式(为了兼容老的项目,更为了维护自己的面子)。将 Spring 与 AspectJ 集成与直接使用 AspectJ 是不同的,我们不需要定义 AspectJ 类(它是扩展了 Java 语法的一种新的语言,还需要特定的编译器),只需要使用 AspectJ 切点表达式即可(它是比正则表达式更加友好的表现形式)。
12. Spring + AspectJ(基于注解:通过 AspectJ execution 表达式拦截方法)
下面以一个最简单的例子,实现之前提到的环绕增强。先定义一个 Aspect 切面类:
03 |
public class GreetingAspect {
|
05 |
@Around ( "execution(* aop.demo.GreetingImpl.*(..))" )
|
06 |
public Object around(ProceedingJoinPoint pjp) throws Throwable {
|
08 |
Object result = pjp.proceed();
|
13 |
private void before() {
|
14 |
System.out.println( "Before" );
|
17 |
private void after() {
|
18 |
System.out.println( "After" );
|
注意:类上面标注的 @Aspect 注解,这表明该类是一个 Aspect(其实就是 Advisor)。该类无需实现任何的接口,只需定义一个方法(方法叫什么名字都无所谓),只需在方法上标注 @Around 注解,在注解中使用了 AspectJ 切点表达式。方法的参数中包括一个 ProceedingJoinPoint 对象,它在 AOP 中称为 Joinpoint(连接点),可以通过该对象获取方法的任何信息,例如:方法名、参数等。
下面重点来分析一下这个切点表达式:
execution(* aop.demo.GreetingImpl.*(..))
- execution():表示拦截方法,括号中可定义需要匹配的规则。
- 第一个“*”:表示方法的返回值是任意的。
- 第二个“*”:表示匹配该类中所有的方法。
- (..):表示方法的参数是任意的。
是不是比正则表达式的可读性更强呢?如果想匹配指定的方法,只需将第二个“*”改为指定的方法名称即可。
如何配置呢?看看是有多简单吧:
01 |
<?xml version= "1.0" encoding= "UTF-8" ?>
|
02 |
<beans xmlns= "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 |
xmlns:aop= "http://www.springframework.org/schema/aop"
|
06 |
xsi:schemaLocation="http:
|
13 |
<context:component-scan base- package = "aop.demo" />
|
15 |
<aop:aspectj-autoproxy proxy-target- class = "true" />
|
两行配置就行了,不需要配置大量的代理,更不需要配置大量的切面,真是太棒了!需要注意的是 proxy-target-class="true" 属性,它的默认值是 false,默认只能代理接口(使用 JDK 动态代理),当为 true 时,才能代理目标类(使用 CGLib 动态代理)。
Spring 与 AspectJ 结合的威力远远不止这些,我们来点时尚的吧,拦截指定注解的方法怎么样?
13. Spring + AspectJ(基于注解:通过 AspectJ @annotation 表达式拦截方法)
为了拦截指定的注解的方法,我们首先需要来自定义一个注解:
1 |
@Target (ElementType.METHOD)
|
2 |
@Retention (RetentionPolicy.RUNTIME)
|
3 |
public @interface Tag {
|
以上定义了一个 @Tag 注解,此注解可标注在方法上,在运行时生效。
只需将前面的 Aspect 类的切点表达式稍作改动:
03 |
public class GreetingAspect {
|
05 |
@Around ( "@annotation(aop.demo.Tag)" )
|
06 |
public Object around(ProceedingJoinPoint pjp) throws Throwable {
|
这次使用了 @annotation() 表达式,只需在括号内定义需要拦截的注解名称即可。
直接将 @Tag 注解定义在您想要拦截的方法上,就这么简单:
2 |
public class GreetingImpl implements Greeting {
|
6 |
public void sayHello(String name) {
|
7 |
System.out.println( "Hello! " + name);
|
以上示例中只有一个方法,如果有多个方法,我们只想拦截其中某些时,这种解决方案会更加有价值。
除了 @Around 注解外,其实还有几个相关的注解,稍微归纳一下吧:
- @Before:前置增强
- @After:后置增强
- @Around:环绕增强
- @AfterThrowing:抛出增强
- @DeclareParents:引入增强
此外还有一个 @AfterReturning(返回后增强),也可理解为 Finally 增强,相当于 finally 语句,它是在方法结束后执行的,也就说说,它比 @After 还要晚一些。
最后一个 @DeclareParents 竟然就是引入增强!为什么不叫做 @Introduction 呢?我也不知道为什么,但它干的活就是引入增强。
14. Spring + AspectJ(引入增强)
为了实现基于 AspectJ 的引入增强,我们同样需要定义一个 Aspect 类:
3 |
public class GreetingAspect {
|
5 |
@DeclareParents (value = "aop.demo.GreetingImpl" , defaultImpl = ApologyImpl. class )
|
6 |
private Apology apology;
|
只需要在 Aspect 类中定义一个需要引入增强的接口,它也就是运行时需要动态实现的接口。在这个接口上标注了 @DeclareParents 注解,该注解有两个属性:
- value:目标类
- defaultImpl:引入接口的默认实现类
我们只需要对引入的接口提供一个默认实现类即可完成引入增强:
1 |
public class ApologyImpl implements Apology {
|
4 |
public void saySorry(String name) {
|
5 |
System.out.println( "Sorry! " + name);
|
以上这个实现会在运行时自动增强到 GreetingImpl 类中,也就是说,无需修改 GreetingImpl 类的代码,让它去实现 Apology 接口,我们单独为该接口提供一个实现类(ApologyImpl),来做 GreetingImpl 想做的事情。
还是用一个客户端来尝试一下吧:
03 |
public static void main(String[] args) {
|
04 |
ApplicationContext context = new ClassPathXmlApplicationContext( "aop/demo/spring.xml" );
|
05 |
Greeting greeting = (Greeting) context.getBean( "greetingImpl" );
|
06 |
greeting.sayHello( "Jack" );
|
08 |
Apology apology = (Apology) greeting;
|
09 |
apology.saySorry( "Jack" );
|
从 Spring ApplicationContext 中获取 greetingImpl 对象(其实是个代理对象),可转型为自己静态实现的接口 Greeting,也可转型为自己动态实现的接口 Apology,切换起来非常方便。
使用 AspectJ 的引入增强比原来的 Spring AOP 的引入增强更加方便了,而且还可面向接口编程(以前只能面向实现类),这也算一个非常巨大的突破。
这一切真的已经非常强大也非常灵活了!但仍然还是有用户不能尝试这些特性,因为他们还在使用 JDK 1.4(根本就没有注解这个东西),怎么办呢?没想到 Spring AOP 为那些遗留系统也考虑到了。
15. Spring + AspectJ(基于配置)
除了使用 @Aspect 注解来定义切面类以外,Spring AOP 也提供了基于配置的方式来定义切面类:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?>
|
04 |
< bean id = "greetingImpl" class = "aop.demo.GreetingImpl" />
|
06 |
< bean id = "greetingAspect" class = "aop.demo.GreetingAspect" />
|
09 |
< aop:aspect ref = "greetingAspect" >
|
10 |
< aop:around method = "around" pointcut = "execution(* aop.demo.GreetingImpl.*(..))" />
|
使用 <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 的整体架构:
相关推荐
在描述中提到的"AOP java_aop_backbone",这可能指的是在Java环境中使用AOP技术,并且可能与Backbone.js这个JavaScript库有关。Backbone.js是一个轻量级的MV*框架,用于构建富前端应用。然而,通常Backbone.js与后端...
1. Spring_AOP_DynProxy:这可能包含有关Spring动态代理的资料,Spring使用JDK动态代理或CGLIB来创建代理对象,实现AOP的功能。 2. ch20_aop_annotation:这部分内容可能是关于使用注解来定义和实现切面的教程。 3. ...
`AOPDemo-master_aop_DEMO_android_`这个项目很可能是为了演示如何在Android环境中应用AOP技术。下面我们将详细探讨AOP在Android开发中的具体应用和实现方式。 1. **注解驱动的AOP**:在Java和Android中,AOP通常是...
《深入解析Spring AOP:源码解读与应用实践》 Spring AOP,即Spring的面向切面编程,是Spring框架的重要组成部分,它提供了一种在不修改源代码的情况下,对程序进行功能增强或统一处理的方法。本文将围绕Spring AOP...
在Java世界里,AOP通常用于实现诸如日志记录、性能监控、事务管理等横切关注点,从而提高代码的模块化和可维护性。 AOP Alliance的核心在于它的接口,这些接口定义了如何声明和执行切面。其中最著名的接口包括`org....
AOP的主要目标是解耦关注点,将核心业务逻辑与系统级别的服务(如日志、事务管理、性能统计等)分离。 在Spring AOP中,有两种主要的实现方式:代理模式和CGLIB。代理模式是基于接口的,当目标对象实现了接口时,...
spring对AOP的支持 1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP 2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP 3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK...
在IT行业中,Spring AOP(面向切面编程)是一个重要的模块,它允许开发者在不修改源代码的情况下,通过“切面”来实现关注点的分离。AOP联盟(AOP Alliance)提供了一套通用的接口,使得不同的AOP框架可以进行互操作...
AOP是一种编程范式,它允许程序员定义“切面”,这些切面可以模块化关注点,如日志、事务管理、安全性等。在Spring中,AOP通过代理实现,可以是JDK动态代理或CGLIB代理。切面由两部分组成:切点(Pointcut)和通知...
首先,Spring AOP的核心概念包括切面(Aspect)、连接点(Join Point)、通知(Advice)、切入点(Pointcut)和织入(Weaving)。切面是关注点的模块化,比如日志、事务管理;连接点是在程序执行过程中可以插入切面...
java aop bible must to read it
在Spring AOP中,切面通常由一个或多个通知(advice)和一个切入点(pointcut)定义组成。 2. **通知(Advice)**:通知是在特定连接点(join point)执行的代码,也就是切面逻辑的实际实现。Spring支持五种不同...
而在Spring框架中,AOP(Aspect Oriented Programming,面向切面编程)是其核心特性之一,它为开发者提供了处理横切关注点的优雅方式。本文将详细介绍Spring AOP4的相关知识点,帮助你深入理解这一强大的编程模型。 ...
面向切面编程(AOP)是Spring框架的另一大特色,它允许程序员将关注点分离,比如日志记录、事务管理、安全检查等,可以编写一次,然后在整个应用中到处使用。AOP通过切面(Aspect)和通知(Advice)来实现。 1. **...
Java AOP(面向切面编程)是软件设计中的一个重要概念,它允许程序员定义“切面”,这些切面封装了特定的、与业务逻辑不直接相关的关注点,如日志、事务管理、性能监控等。AOP的核心思想是将横切关注点从核心业务...
在Java编程领域,AOP(Aspect Oriented Programming,面向切面编程)是一种强大的设计模式,它允许程序员将关注点从核心业务逻辑中分离出来,如日志、事务管理、性能监控等。AOP的主要思想是将这些横切关注点与主...
Spring AOP,全称为Aspect-Oriented Programming(面向切面编程),是Spring框架的重要组成部分,它为Java应用程序提供了强大的横切关注点管理能力。在Java Web开发中,Spring AOP通常用于实现如日志记录、权限控制...
AOP(Aspect Oriented Programming,面向切面编程)是Java编程领域中的一种设计模式,它扩展了传统的面向对象编程,允许程序员定义“方面”,这些方面可以包含关注点,如日志、事务管理、性能监控等,这些关注点通常...
Castle AOP(面向切面编程)是一种编程范式,它旨在将横切关注点(cross-cutting concerns)从业务逻辑中分离出来,以提高模块化。使用AOP技术可以帮助开发者通过切面的方式来减少代码重复,使得代码更加清晰、易于...
面向切面编程(AOP)是一种编程范式,它将关注点分离,使得系统中的核心业务逻辑与横切关注点(如日志、事务、安全检查等)可以解耦。在Spring框架中,AOP通过代理模式实现,可以是JDK动态代理或CGLIB代理。 1. **...