一起来谈谈 Spring AOP!

23,517

一、什么是AOP?

要谈AOP,那么AOP到底是什么呢?AOP即面向切面编程,相比OOP--面向对象编程,由于面向对象中最基本的单位是类,实例,很自然我们会想到AOP中最基本的单位可能就是所谓的切面了,你可能会问,那切面又是个什么东西,我想说,现在不懂没关系,下面我会讲到。我们先来看一段Spring中关于AOP的定义:

面向切面——Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。

上面谈到,AOP可以分离系统的业务逻辑和系统服务(日志,安全等),这个功能我想是不难明白(原理是使用了代理模式),但关键是为什么要将这两种进行分离呢?或者说这样做有什么好处?

在日常的软件开发中,拿日志来说,一个系统软件的开发都是必须进行日志记录的,不然万一系统出现什么bug,你都不知道是哪里出了问题。举个小栗子,当你开发一个登陆功能,你可能需要在用户登陆前后进行权限校验并将校验信息(用户名,密码,请求登陆时间,ip地址等)记录在日志文件中,当用户登录进来之后,当他访问某个其他功能时,也需要进行合法性校验。想想看,当系统非常地庞大,系统中专门进行权限验证的代码是非常多的,而且非常地散乱,我们就想能不能将这些权限校验、日志记录等非业务逻辑功能的部分独立拆分开,并且在系统运行时需要的地方(连接点)进行动态插入运行,不需要的时候就不理,因此AOP是能够解决这种状况的思想吧!

下图就很直观地展示这个过程:

二、AOP中的基本概念

不得不说,AOP的概念是真的多且难以理解,不过不用担心,聪明的你已经准备好战胜它们了。

  • 通知(Adivce)

通知有5种类型:

  • Before 在方法被调用之前调用

  • After 在方法完成后调用通知,无论方法是否执行成功

  • After-returning 在方法成功执行之后调用通知

  • After-throwing 在方法抛出异常后调用通知

  • Around 通知了好、包含了被通知的方法,在被通知的方法调用之前后调用之后执行自定义的行为

    我们可能会问,那通知对应系统中的代码是一个方法、对象、类、还是接口什么的呢?我想说一点,其实都不是,你可以理解通知就是对应我们日常生活中所说的通知,比如‘某某人,你2019年9月1号来学校报个到’,通知更多地体现一种告诉我们(告诉系统何)何时执行,规定一个时间,在系统运行中的某个时间点(比如抛异常啦!方法执行前啦!),并非对应代码中的方法!并非对应代码中的方法!并非对应代码中的方法!

  • 切点(Pointcut)

    哈哈,这个你可能就比较容易理解了,切点在Spring AOP中确实是对应系统中的方法。但是这个方法是定义在切面中的方法,一般和通知一起使用,一起组成了切面。

  • 连接点(Join point)

    比如:方法调用、方法执行、字段设置/获取、异常处理执行、类初始化、甚至是 for 循环中的某个点

    理论上, 程序执行过程中的任何时点都可以作为作为织入点, 而所有这些执行时点都是 Joint point

    但 Spring AOP 目前仅支持方法执行 (method execution) 也可以这样理解,连接点就是你准备在系统中执行切点和切入通知的地方(一般是一个方法,一个字段)

  • 切面(Aspect)

    切面是切点和通知的集合,一般单独作为一个类。通知和切点共同定义了关于切面的全部内容,它是什么时候,在何时和何处完成功能。

  • 引入(Introduction)

    引用允许我们向现有的类添加新的方法或者属性

  • 织入(Weaving)

    组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

三、Spring中对AOP的支持

首先AOP思想的实现一般都是基于代理模式,在JAVA中一般采用JDK动态代理模式,但是我们都知道,JDK动态代理模式只能代理接口,如果要代理类那么就不行了。因此,Spring AOP 会这样子来进行切换,因为Spring AOP 同时支持 CGLIB、ASPECTJ、JDK动态代理,当你的真实对象有实现接口时,Spring AOP会默认采用JDK动态代理,否则采用cglib代理。

  • 如果目标对象的实现类实现了接口,Spring AOP 将会采用 JDK 动态代理来生成 AOP 代理类;
  • 如果目标对象的实现类没有实现接口,Spring AOP 将会采用 CGLIB 来生成 AOP 代理类——不过这个选择过程对开发者完全透明、开发者也无需关心。

四、说了这么多,来点饼干解解渴!直接上代码!

话说饼干能解渴吗?

  • 定义主题接口,这些接口的方法可以成为我们的连接点
package wokao666.club.aop.spring01;

public interface Subject {
	
	//登陆
	public void login();
	
	//下载
	public void download();
	
}
  • 定义实现类,这是代理模式中真正的被代理人(如果你有参与代购,这个就像你在代购中的角色)
package wokao666.club.aop.spring02;

import wokao666.club.aop.spring01.Subject;

public class SubjectImpl implements Subject {

	public void login() {
		
		System.err.println("借书中...");
		
	}

	public void download() {
		
		System.err.println("下载中...");
		
	}
}
  • 定义切面(切面中有切点和通知)
package wokao666.club.aop.spring01;

import org.aspectj.lang.JoinPoint;

public class PermissionVerification {
 /**
  * 权限校验
  * @param args 登陆参数
  */
	public void canLogin() {
		
		//做一些登陆校验
		System.err.println("我正在校验啦!!!!");
		
	}
	
	/**
	 * 校验之后做一些处理(无论是否成功都做处理)
	 * @param args 权限校验参数
	 */
	public void saveMessage() {
		
		//做一些后置处理
			System.err.println("我正在处理啦!!!!");
	}
	
}
  • 再来看看我们的SpringAOP.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"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd">
        
        <bean id="SubjectImpl1" class="wokao666.club.aop.spring02.SubjectImpl" />
        <bean id="SubjectImpl2" class="wokao666.club.aop.spring02.SubjectImpl" />
        <bean id="PermissionVerification" class="wokao666.club.aop.spring01.PermissionVerification" />
        
        <aop:config>
        <!-- 这是定义一个切面,切面是切点和通知的集合-->
            <aop:aspect id="do" ref="PermissionVerification">
            	<!-- 定义切点 ,后面是expression语言,表示包括该接口中定义的所有方法都会被执行-->
                <aop:pointcut id="point" expression="execution(* wokao666.club.aop.spring01.Subject.*(..))" />
                <!-- 定义通知 -->
                <aop:before method="canLogin" pointcut-ref="point" />
                <aop:after method="saveMessage" pointcut-ref="point" />
            </aop:aspect>
        </aop:config>
</beans>
  • 由于我基于MAVEN构建,来看看我们的pom.xml文件
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>wokao666.club</groupId>
  <artifactId>aop</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>aop</name>
  <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.version>4.3.0.RELEASE</spring.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
            <version>4.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.9</version>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.2.4</version>
        </dependency>
  </dependencies>
</project>
  • 最后看看我们的测试类和结果
package wokao666.club.aop.spring01;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
	
	public static void main(String[] args) {
		ApplicationContext ctx = 
	      new ClassPathXmlApplicationContext("SpringAOP.xml");
	  
				Subject subject1 = (Subject)ctx.getBean("SubjectImpl1");
				Subject subject2 = (Subject)ctx.getBean("SubjectImpl2");
				
				subject1.login();
				subject1.download();
				
				
				System.err.println("==================");
				
				
				
				
				subject1.login();
				subject1.download();
				
				
				
				
	}
}
三月 13, 2018 4:59:44 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@31cefde0: startup date [Tue Mar 13 16:59:44 CST 2018]; root of context hierarchy
三月 13, 2018 4:59:45 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [SpringAOP.xml]
我正在校验啦!!!!
借书中...
我正在处理啦!!!!
我正在校验啦!!!!
下载中...
我正在处理啦!!!!
==================
我正在校验啦!!!!
借书中...
我正在处理啦!!!!
我正在校验啦!!!!
下载中...
我正在处理啦!!!!

我想,上面的实现方式只是皮毛而已啦!如果想要深入学习,那么可以看下JDK动态代理的源码分析(里边非常地巧妙,还运用了二级缓存,本来我想写出来的,就留给下一篇文吧!)、Spring AOP源码分析等等,当然上面的实现方式是基于老式的xml文件,不推荐我们这样做,为什么?我觉得当你系统大了之后,那系统中将会有很多的beanxml文件,这不容易维护和管理,我建议采用基于注解的方式进行AOP编程会更好!

好了,本来想结束的,但是在关掉eclipse前发现了一个小问题,既然切点是需要执行的方法,那么我们如何获取连接点的数据(或者说是校验参数呢?),这里推荐一篇博文,我就不写啦!小伙伴们,一起加油吧!

spring AOP 通知参数的传递