前面两篇文章记录了 Spring IOC 的相关知识,本文记录 Spring 中的另外一特性 AOP 相关知识。html
部分参考资料:
《Spring实战(第4版)》
《轻量级 JavaEE 企业应用实战(第四版)》
Spring 官方文档
W3CSchool Spring教程
易百教程 Spring教程java
AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。
从《Spring实战(第4版)》图书中扒了一张图: spring
从该图能够很形象地看出,所谓切面,至关于应用对象间的横切点,咱们能够将其单独抽象为单独的模块。express
想象下面的场景,开发中在多个模块间有某段重复的代码,咱们一般是怎么处理的?显然,没有人会靠“复制粘贴”吧。在传统的面向过程编程中,咱们也会将这段代码,抽象成一个方法,而后在须要的地方分别调用这个方法,这样当这段代码须要修改时,咱们只须要改变这个方法就能够了。然而需求老是变化的,有一天,新增了一个需求,须要再多出作修改,咱们须要再抽象出一个方法,而后再在须要的地方分别调用这个方法,又或者咱们不须要这个方法了,咱们仍是得删除掉每一处调用该方法的地方。实际上涉及到多个地方具备相同的修改的问题咱们均可以经过 AOP 来解决。编程
AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码,看到这其实应该明白了,AOP 其实就是前面一篇文章讲的代理模式的典型应用。
按照 AOP 框架修改源代码的时机,能够将其分为两类:框架
下面给出经常使用 AOP 实现比较
yii
如不清楚动态代理的,可参考我前面的一篇文章,有讲解静态代理、JDK动态代理和 CGlib 动态代理。
静态代理和动态代理 https://www.cnblogs.com/joy99/p/10865391.htmlide
AOP 领域中的特性术语:测试
概念看起来老是有点懵,而且上述术语,不一样的参考书籍上翻译还不同,因此须要慢慢在应用中理解。gradle
AOP 框架有不少种,1.3节中介绍了 AOP 框架的实现方式有可能不一样, Spring 中的 AOP 是经过动态代理实现的。不一样的 AOP 框架支持的链接点也有所区别,例如,AspectJ 和 JBoss,除了支持方法切点,它们还支持字段和构造器的链接点。而 Spring AOP 不能拦截对对象字段的修改,也不支持构造器链接点,咱们没法在 Bean 建立时应用通知。
下面先上代码,对着代码说比较好说,看下面这个例子: 这个例子是基于gradle建立的,首先 build.gradle 文件添加依赖:
dependencies { compile 'org.springframework:spring-context:5.0.6.RELEASE' }
首先建立一个接口 IBuy.java
package com.sharpcj.aopdemo.test1; public interface IBuy { String buy(); }
Boy 和 Gril 两个类分别实现了这个接口: Boy.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Boy implements IBuy { @Override public String buy() { System.out.println("男孩买了一个游戏机"); return "游戏机"; } }
Girl.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Girl implements IBuy { @Override public String buy() { System.out.println("女孩买了一件漂亮的衣服"); return "衣服"; } }
配置文件, AppConfig.java
package com.sharpcj.aopdemo; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class}) public class AppConfig { }
测试类, AppTest.java
package com.sharpcj.aopdemo; import com.sharpcj.aopdemo.test1.Boy; import com.sharpcj.aopdemo.test1.Girl; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class AppTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Boy boy = context.getBean("boy",Boy.class); Girl girl = (Girl) context.getBean("girl"); boy.buy(); girl.buy(); } }
运行结果:
这里运用SpringIOC里的自动部署。如今需求改变了,咱们须要在男孩和女孩的 buy 方法以前,须要打印出“男孩女孩都买了本身喜欢的东西”。用 Spring AOP 来实现这个需求只需下面几个步骤:
一、 既然用到 Spring AOP, 首先在 build.gralde
文件中引入相关依赖:
dependencies { compile 'org.springframework:spring-context:5.0.6.RELEASE' compile 'org.springframework:spring-aspects:5.0.6.RELEASE' }
二、 定义一个切面类,BuyAspectJ.java
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { @Before("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void haha(){ System.out.println("男孩女孩都买本身喜欢的东西"); } }
这个类,咱们使用了注解 @Component
代表它将做为一个Spring Bean 被装配,使用注解 @Aspect
表示它是一个切面。
类中只有一个方法 haha
咱们使用 @Before
这个注解,表示他将在方法执行以前执行。关于这个注解后文再做解释。
参数("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))")
声明了切点,代表在该切面的切点是com.sharpcj.aopdemo.test1.Ibuy
这个接口中的buy
方法。至于为何这么写,下文再解释。 三、 在配置文件中启用AOP切面功能
package com.sharpcj.aopdemo; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class}) @EnableAspectJAutoProxy(proxyTargetClass = true) public class AppConfig { }
咱们在配置文件类增长了@EnableAspectJAutoProxy
注解,启用了 AOP 功能,参数proxyTargetClass
的值设为了 true 。默认值是 false,二者的区别下文再解释。
OK,下面只需测试代码,运行结果以下:
咱们看到,结果与咱们需求一致,咱们并无修改 Boy 和 Girl 类的 Buy 方法,也没有修改测试类的代码,几乎是彻底无侵入式地实现了需求。这就是 AOP 的“神奇”之处。
Spring AOP 所支持的 AspectJ 切点指示器
在spring中尝试使用AspectJ其余指示器时,将会抛出IllegalArgumentException异常。
当咱们查看上面展现的这些spring支持的指示器时,注意只有execution指示器是惟一的执行匹配,而其余的指示器都是用于限制匹配的。这说明execution指示器是咱们在编写切点定义时最主要使用的指示器,在此基础上,咱们使用其余指示器来限制所匹配的切点。
下图的切点表达式表示当Instrument的play方法执行时会触发通知。
咱们使用execution指示器选择Instrument的play方法,方法表达式以 *
号开始,标识咱们不关心方法的返回值类型。而后咱们指定了全限定类名和方法名。对于方法参数列表,咱们使用 ..
标识切点选择任意的play方法,不管该方法的入参是什么。
多个匹配之间咱们可使用连接符 &&
、||
、!
来表示 “且”、“或”、“非”的关系。可是在使用 XML 文件配置时,这些符号有特殊的含义,因此咱们使用 “and”、“or”、“not”来表示。
举例:
限定该切点仅匹配的包是 com.sharpcj.aopdemo.test1
,可使用 execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && within(com.sharpcj.aopdemo.test1.*)
在切点中选择 bean,可使用
execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && bean(girl)
修改 BuyAspectJ.java
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { @Before("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && within(com.sharpcj.aopdemo.test1.*) && bean(girl)") public void hehe(){ System.out.println("男孩女孩都买本身喜欢的东西"); } }
此时,切面只会对 Girl.java
这个类生效,执行结果:
细心的你,可能发现了,切面中的方法名,已经被我悄悄地从haha
改为了hehe
,丝毫没有影响结果,说明方法名没有影响。和 Spring IOC 中用 java 配置文件装配 Bean 时,用@Bean
注解修饰的方法名同样,没有影响。
Spring AOP 中有 5 中通知类型,分别以下:
下面修改切面类:
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { @Before("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void hehe() { System.out.println("before ..."); } @After("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void haha() { System.out.println("After ..."); } @AfterReturning("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void xixi() { System.out.println("AfterReturning ..."); } @Around("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void xxx(ProceedingJoinPoint pj) { try { System.out.println("Around aaa ..."); pj.proceed(); System.out.println("Around bbb ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
为了方便看效果,咱们测试类中,只要 Boy 类:
package com.sharpcj.aopdemo; import com.sharpcj.aopdemo.test1.Boy; import com.sharpcj.aopdemo.test1.Girl; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class AppTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Boy boy = context.getBean("boy",Boy.class); Girl girl = (Girl) context.getBean("girl"); boy.buy(); // girl.buy(); } }
执行结果以下:
结果显而易见。指的注意的是 @Around
修饰的环绕通知类型,是将整个目标方法封装起来了,在使用时,咱们传入了 ProceedingJoinPoint
类型的参数,这个对象是必需要有的,而且须要调用 ProceedingJoinPoint
的 proceed()
方法。 若是没有调用 该方法,执行结果为 :
Around aaa ... Around bbb ... After ... AfterReturning ...
可见,若是不调用该对象的 proceed() 方法,表示原目标方法被阻塞调用,固然也有可能你的实际需求就是这样。
如你看到的,上面咱们写的多个通知使用了相同的切点表达式,对于像这样频繁出现的相同的表达式,咱们可使用 @Pointcut
注解声明切点表达式,而后使用表达式,修改代码以下:
BuyAspectJ.java
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { @Pointcut("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void point(){} @Before("point()") public void hehe() { System.out.println("before ..."); } @After("point()") public void haha() { System.out.println("After ..."); } @AfterReturning("point()") public void xixi() { System.out.println("AfterReturning ..."); } @Around("point()") public void xxx(ProceedingJoinPoint pj) { try { System.out.println("Around aaa ..."); pj.proceed(); System.out.println("Around bbb ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
程序运行结果没有变化。
这里,咱们使用
@Pointcut("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void point(){}
声明了一个切点表达式,该方法 point 的内容并不重要,方法名也不重要,实际上它只是做为一个标识,供通知使用。
上面的例子,咱们要进行加强处理的目标方法没有参数,下面咱们来讲说有参数的状况,而且在加强处理中使用该参数。 下面咱们给接口增长一个参数,表示购买所花的金钱。经过AOP 加强处理,若是女孩买衣服超过了 68 元,就能够赠送一双袜子。 更改代码以下: IBuy.java
package com.sharpcj.aopdemo.test1; public interface IBuy { String buy(double price); }
Girl.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Girl implements IBuy { @Override public String buy(double price) { System.out.println(String.format("女孩花了%s元买了一件漂亮的衣服", price)); return "衣服"; } }
Boy.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Boy implements IBuy { @Override public String buy(double price) { System.out.println(String.format("男孩花了%s元买了一个游戏机", price)); return "游戏机"; } }
再看 BuyAspectJ 类,咱们将以前的通知都注释掉。用一个环绕通知来实现这个功能:
package com.sharpcj.aopdemo.test1; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class BuyAspectJ { /* @Pointcut("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))") public void point(){} @Before("point()") public void hehe() { System.out.println("before ..."); } @After("point()") public void haha() { System.out.println("After ..."); } @AfterReturning("point()") public void xixi() { System.out.println("AfterReturning ..."); } @Around("point()") public void xxx(ProceedingJoinPoint pj) { try { System.out.println("Around aaa ..."); pj.proceed(); System.out.println("Around bbb ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } */ @Pointcut("execution(String com.sharpcj.aopdemo.test1.IBuy.buy(double)) && args(price) && bean(girl)") public void gif(double price) { } @Around("gif(price)") public String hehe(ProceedingJoinPoint pj, double price){ try { pj.proceed(); if (price > 68) { System.out.println("女孩买衣服超过了68元,赠送一双袜子"); return "衣服和袜子"; } } catch (Throwable throwable) { throwable.printStackTrace(); } return "衣服"; } }
前文提到,当不关心方法返回值的时候,咱们在编写切点指示器的时候使用了 *
, 当不关心方法参数的时候,咱们使用了 ..
。如今若是咱们须要传入参数,而且有返回值的时候,则须要使用对应的类型。在编写通知的时候,咱们也须要声明对应的返回值类型和参数类型。
测试类:AppTest.java
package com.sharpcj.aopdemo; import com.sharpcj.aopdemo.test1.Boy; import com.sharpcj.aopdemo.test1.Girl; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class AppTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Boy boy = context.getBean("boy",Boy.class); Girl girl = (Girl) context.getBean("girl"); String boyBought = boy.buy(35); String girlBought = girl.buy(99.8); System.out.println("男孩买到了:" + boyBought); System.out.println("女孩买到了:" + girlBought); } }
测试结果:
能够看到,咱们成功经过 AOP 实现了需求,并将结果打印了出来。
前面还有一个遗留问题,在配置文件中,咱们用注解 @EnableAspectJAutoProxy()
启用Spring AOP 的时候,咱们给参数 proxyTargetClass
赋值为 true
,若是咱们不写参数,默认为 false。这个时候运行程序,程序抛出异常
这是一个强制类型转换异常。为何会抛出这个异常呢?或许已经可以想到,这跟Spring AOP 动态代理的机制有关,这个 proxyTargetClass
参数决定了代理的机制。当这个参数为 false 时, 经过jdk的基于接口的方式进行织入,这时候代理生成的是一个接口对象,将这个接口对象强制转换为实现该接口的一个类,天然就抛出了上述类型转换异常。 反之,proxyTargetClass
为 true
,则会使用 cglib 的动态代理方式。这种方式的缺点是拓展类的方法被final
修饰时,没法进行织入。
测试一下,咱们将 proxyTargetClass
参数设为 true
,同时将 Girl.java 的 Buy 方法用 final
修饰:
AppConfig.java
package com.sharpcj.aopdemo; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class}) @EnableAspectJAutoProxy(proxyTargetClass = true) public class AppConfig { }
Girl.java
package com.sharpcj.aopdemo.test1; import org.springframework.stereotype.Component; @Component public class Girl implements IBuy { @Override public final String buy(double price) { System.out.println(String.format("女孩花了%s元买了一件漂亮的衣服", price)); return "衣服"; } }
此时运行结果:
能够看到,咱们的切面并无织入生效。
前面的示例中,咱们已经展现了如何经过注解配置去声明切面,下面咱们看看如何在 XML 文件中声明切面。下面先列出 XML 中声明 AOP 的经常使用元素:
咱们依然可使用 <aop:aspectj-autoproxy>
元素,他可以自动代理AspectJ注解的通知类。
在XML配置文件中,切点指示器表达式与经过注解配置的写法基本一致,区别前面有提到,即XML文件中须要使用 “and”、“or”、“not”来表示 “且”、“或”、“非”的关系。
下面咱们不使用任何注解改造上面的例子:
BuyAspectJ.java
package com.sharpcj.aopdemo.test2; import org.aspectj.lang.ProceedingJoinPoint; public class BuyAspectJ { public void hehe() { System.out.println("before ..."); } public void haha() { System.out.println("After ..."); } public void xixi() { System.out.println("AfterReturning ..."); } public void xxx(ProceedingJoinPoint pj) { try { System.out.println("Around aaa ..."); pj.proceed(); System.out.println("Around bbb ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } }
在 Resource 目录下新建一个配置文件 aopdemo.xml :
<?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"> <bean id="boy" class="com.sharpcj.aopdemo.test2.Boy"></bean> <bean id="girl" class="com.sharpcj.aopdemo.test2.Girl"></bean> <bean id="buyAspectJ" class="com.sharpcj.aopdemo.test2.BuyAspectJ"></bean> <aop:config proxy-target-class="true"> <aop:aspect id="qiemian" ref="buyAspectJ"> <aop:before pointcut="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))" method="hehe"/> <aop:after pointcut="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))" method="haha"/> <aop:after-returning pointcut="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))" method="xixi"/> <aop:around pointcut="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))" method="xxx"/> </aop:aspect> </aop:config> </beans>
这里分别定义了一个切面,里面包含四种类型的通知。 测试文件中,使用
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("aopdemo.xml");
来获取 ApplicationContext,其它代码不变。
对于频繁重复使用的切点表达式,咱们也能够声明成切点。 配置文件以下:aopdemo.xml
<?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"> <bean id="boy" class="com.sharpcj.aopdemo.test2.Boy"></bean> <bean id="girl" class="com.sharpcj.aopdemo.test2.Girl"></bean> <bean id="buyAspectJ" class="com.sharpcj.aopdemo.test2.BuyAspectJ"></bean> <aop:config proxy-target-class="true"> <aop:pointcut id="apoint" expression="execution(* com.sharpcj.aopdemo.test2.IBuy.buy(..))"/> <aop:aspect id="qiemian" ref="buyAspectJ"> <aop:before pointcut-ref="apoint" method="hehe"/> <aop:after pointcut-ref="apoint" method="haha"/> <aop:after-returning pointcut-ref="apoint" method="xixi"/> <aop:around pointcut-ref="apoint" method="xxx"/> </aop:aspect> </aop:config> </beans>
BuyAspectJ.java
package com.sharpcj.aopdemo.test2; import org.aspectj.lang.ProceedingJoinPoint; public class BuyAspectJ { public String hehe(ProceedingJoinPoint pj, double price){ try { pj.proceed(); if (price > 68) { System.out.println("女孩买衣服超过了68元,赠送一双袜子"); return "衣服和袜子"; } } catch (Throwable throwable) { throwable.printStackTrace(); } return "衣服"; } }
aopdemo.xml
<?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"> <bean id="boy" class="com.sharpcj.aopdemo.test2.Boy"></bean> <bean id="girl" class="com.sharpcj.aopdemo.test2.Girl"></bean> <bean id="buyAspectJ" class="com.sharpcj.aopdemo.test2.BuyAspectJ"></bean> <aop:config proxy-target-class="true"> <aop:pointcut id="apoint" expression="execution(String com.sharpcj.aopdemo.test2.IBuy.buy(double)) and args(price) and bean(girl)"/> <aop:aspect id="qiemian" ref="buyAspectJ"> <aop:around pointcut-ref="apoint" method="hehe"/> </aop:aspect> </aop:config> </beans>
同注解配置相似,
CGlib 代理方式:
<aop:config proxy-target-class="true"> </aop:config>
JDK 代理方式:
<aop:config proxy-target-class="false"> </aop:config>
本文简单记录了 AOP 的编程思想,而后介绍了 Spring 中 AOP 的相关概念,以及经过注解方式和XML配置文件两种方式使用 Spring AOP进行编程。 相比于 AspectJ 的面向切面编程,Spring AOP 也有一些局限性,可是已经能够解决开发中的绝大多数问题了,若是确实遇到了 Spring AOP 解决不了的场景,咱们依然能够在 Spring 中使用 AspectJ 来解决。
原文出处:https://www.cnblogs.com/joy99/p/10941543.html