在云笔记项目的过程中,需要检查各个业务层的执行快慢,如登录、注册、展示笔记本列表,展示笔记列表等,如果在每个业务层方法里都写一段代码用来检查时间并打印,不仅仅显得代码重复,而且当项目很大的时候,将大大加大工作量。这个时候AOP的概念引入了,本文在引用其他大牛博文的基础上,对AOP知识进行了简单整理,今后可以参考使用。
什么是AOP
AOP(Aspect Oriented Programming),即面向切面编程,底层使用了动态代理技术,在不改变原有业务逻辑的基础上,横向添加业务逻辑。就云笔记项目来说,任何一个Action,都会按照浏览器<-->控制层<-->业务层<-->持久层<-->数据库的顺序执行,每一个小功能都是按照此路程进行,这个可以理解为纵向逻辑。然后上面说的检查业务层方法执行快慢的功能,因为其分布在各个业务层里,像横切了一刀,因此形象的叫做切面。AOP是对OOP(Object Oriented Programming)的补充,它利用"横切"的技术,解开封装对象的内部,将影响到多个类的公共行为(如测试某个业务响应时间、事务管理、日志记录、异常处理等),封装到一个可以重用的模块,并将其取名“Aspect”,即切面。
使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点发生在核心关注点的多个地方,功能基本相似,如上述的权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。另外AOP实现了"高内聚",而IOC和DI实现了"低耦合"。
主要概念
(1)横切关注点
对哪些方法进行拦截,拦截后如何处理,这些关注点为横切关注点
(2)连接点(Joinpoint)
在Spring中连接点就是被拦截到的方法,也可以是字段或者构造器(暂时没实践过)
(3)切入点(Pointcut)
需配合切入表达式使用,切入点是符合切入规则的连接点,可以对选择出来的连接点进行功能的增强。主要有bean组件切入点,类切入点和方法切入点:
(a)bean组件切入点,语法 bean(beanid),以云笔记为例,bean(userService) 切入一个类,bean(userService)||bean(noteService)||bean(noteBookService) 切入三个类,bean(userService)||bean(noteService)||bean(noteBookService) 切入三个类,bean(*Service) 切入所有后缀名字为Service的类。
(b)类切入点,语法 within(包名.类名),以云笔记为例,如within(com.boe.Service.UserServiceImpl),within(com.boe.Service.UserServiceImpl)||within(com.boe.Service.noteBookServiceImpl),within(com.boe.*.*ServiceImpl)。
(c) 方法切入点,语法 execution(返回值类型 包名.类名.方法名(参数类型)),比如execution(* com.boe.Service.UserServiceImpl.login(..)),代表返回类型不限定,com.boe.Service包下类UserServiceImpl,方法名为login,参数个数和类型不限定。
以上三种切入点方式,只有方法切入点是细粒度的,可以更加精细的定位到切入点。
(4)切面(Aspect)
事物的横切面,是对横切关注点的抽象,简单理解就是Spring将拦截下来的切入点交给一个处理类来处理,进行功能的增强,这个处理类就是一个横切面。
(5)通知(Advice)
Spring拦截到连接点变成切入点交给切面类,切面类中有处理切入点的方法,这些方法就是通知(也称增强方法),按照类型来划分主要有@Before,@After,@AfterReturning,@AfterThrowing和@Around五种。
五种通知的大致执行顺序可以根据try-catch-finally来理解,不过不是完全相同,如发生异常,@After注解的方法会执行,@AfterReturning注解的方法没机会执行。
1 try{ 2 @Before 方法执行 3 目标业务方法执行 4 @AfterReturning 方法执行 5 }catch(Exception e){ 6 @AfterThrowing 如果有异常将执行 7 }finally{ 8 @After 方法执行 9 }
(6)目标对象(Target Object)
被一个或多个切面所通知(在切入点增强)的对象,就是目标对象,Spring AOP底层是代理实现,目标对象其实也是被代理对象。
(7)织入(Weave)
将切面应用到目标对象后代理对象创建的过程
Spring AOP
简单来说,Spring AOP底层调用了AspectJ AOP,而AspectJ AOP底层使用动态代理。有两种动态代理技术,一种是使用JDK动态代理,一种是使用CGLib动态代理。两种的区别在于目标方法是否有对应的接口,如果目标方法有对应的接口就使用JDK动态代理,如果目标方法没有接口就使用CGLib,使用CGLib需要导入第三方的包才能使用,如果使用不带注解的方式进行aop配置,可以在<aop:config>内配置“proxy-target-class”属性,默认情况下为false,如果设置为true,将使用CGLib代理,具体底层暂时水平不足无法深究,后续可能补充。
接下来的例子将使用JDK动态代理,因为目标方法有对应的接口。
Spring会帮忙动态创建对象并帮忙管理对象之间的依赖关系,因此使用Spring将大大简化AOP的使用过程,如果使用底层AspectJ AOP将加大代码量。使用AOP主要要准备确认好以下几点:
(1)确定普通业务组件
(2)确定切入点
(3)确定AOP业务组件,为普通业务组件织入增强处理
基于Spring AOP的简单应用
Spring AOP的简单应用,将以使用注解和不使用注解两种方式进行简单使用。不管使用哪种方式,都需要导入aspectjweaver.jar,大牛博客介绍说要导入aopalliance.jar,暂时这个未做导入,也可以测试通过,后续了解。以下是Maven项目下pom.xml的配置:
1 <dependency> 2 <groupId>aspectj</groupId> 3 <artifactId>aspectjweaver</artifactId> 4 <version>1.5.3</version> 5 </dependency>
case 1 使用注解的情况
(1)定义接口
1 package Test; 2 3 public interface HelloAOP { 4 5 public void helloAop() throws InterruptedException; 6 7 }
(2)定义接口的实现类
1 package Test; 2 3 import org.springframework.stereotype.Component; 4 5 @Component("helloAOPImpl1") 6 public class HelloAOPImpl1 implements HelloAOP{ 7 8 public void helloAop() throws InterruptedException { 9 //特地让其执行1s 10 Thread.sleep(1000); 11 System.out.println("Hello Aop From HelloAOPImpl1"); 12 } 13 }
(3)定义横切关注点,拦截后打印两个时间
package AOP; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; /** * 带注解的AOP,打印时间 * @author clyang */ @Component @Aspect public class AOP1 { /** * 使用bean组件切入点 */ @Before("bean(helloAOPImpl1)") public void testBefore() { System.out.println("当前时间为:"+System.currentTimeMillis()); } @After("bean(helloAOPImpl1)") public void testAfter() { System.out.println("当前时间为:"+System.currentTimeMillis()); } }
(4)Sprng-aop1.xml中配置,按照有注解的方式进行配置
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:jee="http://www.springframework.org /schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" 6 xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:mvc="http://www.springframework.org/schema/mvc" 7 xsi:schemaLocation=" 8 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd 9 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd 10 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd 11 http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd 12 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd 13 http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd 14 http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd 15 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> 16 17 <!-- 使用注解的方式 --> 18 <!-- 配置组件扫描,使得@Component注解生效--> 19 <context:component-scan base-package="AOP"></context:component-scan> 20 <context:component-scan base-package="Test"></context:component-scan> 21 <!-- 注解驱动 --> 22 <mvc:annotation-driven></mvc:annotation-driven> 23 24 <!-- 添加aop注解驱动,使得@Aspect注解生效 --> 25 <!-- 需要在头文件中添加“xmlns:aop”的命名申明,并在“xsi:schemaLocation”中指定aop配置的schema的地址 --> 26 <aop:aspectj-autoproxy /> 27 </beans>
(5)写一个测试类进行测试
1 package TestAOP; 2 3 import org.junit.Before; 4 import org.junit.Test; 5 import org.springframework.context.ApplicationContext; 6 import org.springframework.context.support.ClassPathXmlApplicationContext; 7 8 import Test.HelloAOP; 9 10 public class TestCase { 11 ApplicationContext ac1=null;//使用注解 12 13 @Before 14 public void before() { 15 String config1="config/spring-aop1.xml"; 16 ac1=new ClassPathXmlApplicationContext(config1); 17 } 18 19 /** 20 * 使用注解 21 * @throws InterruptedException 22 */ 23 @Test 24 public void test() throws InterruptedException { 25 //测试执行业务方法时,切面方法是否执行 27 HelloAOP helloAop=ac1.getBean("helloAOPImpl1",HelloAOP.class); 28 helloAop.helloAop(); 29 } 30 31 }
(6)运行结果为:
当前时间为:1553328677357
Hello Aop From HelloAOPImpl1
当前时间为:1553328678357
发现目标方法打印出结果的前后,分别调用@Before和@After的通知,执行了打印当前时间,并延迟1s打印执行后的时间。
(7)小花絮
当时在测试类里,本来想通过@Resource或者@Autowired配合@Qualifier注入helloAOP实现类实体对象,结果都返回为null,参考网上博客,可能的原因分析如下:
(1)如果使用main方法执行测试,@Resource注解以及具体执行方法,会封装在一个类里(假设类名为T)。然后main方法通过实例化这个类T,来调用它里面的执行方法,这种情况因为new关键字实例化了测试类T,测试类对象就不归Spring容器管理,导致测试类对象使用注解注入helloAOPImpl1失效。
(2)如果使用Junit测试,原理跟main方法类似。
case 2 不使用注解的情况
(1)定义接口的实现类
1 package Test; 2 3 public class HelloAOPImpl2 implements HelloAOP{ 4 5 public void helloAop() throws InterruptedException { 6 //特地让其执行1s 7 Thread.sleep(1000); 8 System.out.println("Hello Aop From HelloAOPImpl2"); 9 } 10 }
(2)定义横切关注点,拦截后打印两个时间
1 package AOP; 2 3 import org.aspectj.lang.annotation.After; 4 import org.aspectj.lang.annotation.Before; 5 6 /** 7 * 不带注解的AOP 打印时间 8 * @author clyang 9 * 10 */ 11 public class AOP2 { 12 13 public void testBefore() { 14 System.out.println("当前时间为:"+System.currentTimeMillis()); 15 } 16 17 public void testAfter() { 18 System.out.println("当前时间为:"+System.currentTimeMillis()); 19 } 20 }
(3)Sprng-aop2.xml中配置,按照没有注解的方式进行配置
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:jee="http://www.springframework.org /schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" 6 xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:mvc="http://www.springframework.org/schema/mvc" 7 xsi:schemaLocation=" 8 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd 9 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd 10 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd 11 http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd 12 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd 13 http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd 14 http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd 15 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> 16 17 <!-- 不使用注解的方式--> 18 <bean id="aop2" class="AOP.AOP2"></bean> 19 <bean id="helloAopImpl2" class="Test.HelloAOPImpl2"></bean> 20 21 <!-- 1个切面配置 --> 22 <aop:config> 23 <aop:aspect id="test" ref="aop2"> 24 <aop:pointcut id="time" expression="bean(helloAopImpl2)"/> <!-- 使用bean组件切入点 --> 25 <aop:before method="testBefore" pointcut-ref="time"/> 26 <aop:after method="testAfter" pointcut-ref="time"/> 27 </aop:aspect> 28 </aop:config> 29 30 </beans>
(4)测试类修改进行测试
1 package TestAOP; 2 3 import org.junit.Before; 4 import org.junit.Test; 5 import org.springframework.context.ApplicationContext; 6 import org.springframework.context.support.ClassPathXmlApplicationContext; 7 8 import Test.HelloAOP; 9 10 public class TestCase { 11 ApplicationContext ac1=null;//使用注解 12 ApplicationContext ac2=null;//不使用注解 13 14 @Before 15 public void before() { 16 String config1="config/spring-aop1.xml"; 17 String config2="config/spring-aop2.xml"; 18 ac1=new ClassPathXmlApplicationContext(config1); 19 ac2=new ClassPathXmlApplicationContext(config2); 20 } 21 22 /** 23 * 使用注解 24 * @throws InterruptedException 25 */ 26 @Test 27 public void test() throws InterruptedException { 28 //测试执行业务方法时,切面方法是否执行 29 //helloAop.helloAop(); 30 31 HelloAOP helloAop=ac1.getBean("helloAOPImpl1",HelloAOP.class); 32 helloAop.helloAop(); 33 } 34 /** 35 * 不使用注解 36 * @throws InterruptedException 37 */ 38 @Test 39 public void test1() throws InterruptedException { 40 HelloAOP helloAop=ac2.getBean("helloAopImpl2",HelloAOP.class); 41 helloAop.helloAop(); 42 } 43 44 }
(5)单独执行test1方法进行测试,测试结果为:
当前时间为:1553329722057
Hello Aop From HelloAOPImpl2
当前时间为:1553329723058
Spring AOP使用的简单对比
(1)如果使用注解,使用过程很简单,配置文件配置好注解扫描和Aspect自动动态代理扫描,Spring容器启动后就可以扫描到业务类和AOP类,通过通知注解,完成对目标方法的拦截,实现横切。
(2)如果不使用注解,需要配置文件中管理bean,对业务类和AOP类进行手动管理,并在里面进行具体AOP配置。相对来说使用要稍微复杂一些。以下再参考大牛博文进行一点补充,如何在没有注解的情况下配置AOP。
使用注解配置的三种方式
(1)在<aop:aspect>标签内使用<aop:pointcut>声明切入点,该切入点一般只被该切面使用,expression是切入点方式,如上文有三种来写,这里写bean组件切入点。切入点使用id属性指定bean名字,在通知定义时使用pointcut-ref来引入切入点。当执行到切入点后会激活通知里对应的方法,如本例中的testBefore()方法和testAfter()方法。
<aop:config> <aop:aspect id="test" ref="aop2"> <aop:pointcut id="time" expression="bean(helloAopImpl2)"/> <!-- 使用bean组件切入点 --> <aop:before method="testBefore" pointcut-ref="time"/> <aop:after method="testAfter" pointcut-ref="time"/> </aop:aspect> </aop:config>
(2)在<aop:config>标签内使用<aop:pointcut>声明切入点,这个切入点可以被多个切面使用,同样对切入点进行命名,在通知定义时通过bean id来引入切入点。
<aop:config> <aop:pointcut id="time" expression="bean(helloAopImpl2)"/> <!-- 使用bean组件切入点 --> <aop:aspect id="test" ref="aop2"> <aop:before method="testBefore" pointcut-ref="time"/> <aop:after method="testAfter" pointcut-ref="time"/> </aop:aspect> </aop:config>
(3)匿名切入点Bean,在通知声明时通过pointcut属性指定切入点表达式,该切入点只被该通知使用
<aop:config> <aop:aspect id="test" ref="aop2"> <aop:before method="testBefore" pointcut="bean(helloAopImpl2)"/> <aop:after method="testAfter" pointcut="bean(helloAopImpl2)"/> </aop:aspect> </aop:config>
本文中使用的是第一种方式。
总结
(1)AOP在需要横向扩展功能时非常有效,其底层使用了动态代理技术,动态代理技术底层使用过了反射的技术。
(2)AOP可以使用两种配置方式来使用,带注解和不带注解的方式,并且不带注解的方式还有多种配置AOP的写法。
(3)AOP可以使用在云笔记项目中进行业务层的性能测试。
参考博文:
(1)https://www.cnblogs.com/xrq730/p/4919025.html