云笔记项目-AOP知识简单学习

在云笔记项目的过程中,需要检查各个业务层的执行快慢,如登录、注册、展示笔记本列表,展示笔记列表等,如果在每个业务层方法里都写一段代码用来检查时间并打印,不仅仅显得代码重复,而且当项目很大的时候,将大大加大工作量。这个时候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

(2)https://www.cnblogs.com/mxck/p/7027912.html

(3)https://www.cnblogs.com/youngchaolin/p/11594869.html

原文地址:https://www.cnblogs.com/youngchaolin/p/10578462.html