一、AOP概念的引入
为了更好地介绍AOP,我们以登录作为示例。
首先,我们先来看一下登录的原理:
如图所示,这是一个基本的登录原理图,但是如果我们想要在这个登录过程上再添加一些新的功能,比如权限校验,那么我们能想到的有两种方法:
- 通过对源代码的修改来实现添加新功能
- 不修改源代码,而是将新功能代码直接切入来实现添加新功能。即下图:
二、AOP相关概念
1、AOP的概述
(1)什么是AOP的技术?
在软件业中,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程。
AOP是一种编程范式,隶属于软工范畴,指导开发者如何组织程序结构。AOP最早由AOP联盟的组织提出,并制定了一套规范。Spring将AOP思想引入到框架中,就必须遵守AOP联盟的规范。
Spring中的AOP技术是通过预编译方式或者运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
(2)为什么要学习AOP?
因为运行期间,AOP可以在不修改源代码的情况下对已有的方法进行增强。
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码(事务管理、安全检查、缓存)。
2. AOP的优势
- 减少重复的代码
- 提供开发的效率
- 维护方便
3. AOP的底层原理
(1)JDK的动态代理技术
JDK动态代理(必须要有接口),生成代理对象实现相同的接口,JDK动态代理底层采用接口的方式,实现增强。
- 为接口创建代理类的字节码文件
- 使用ClassLoader将字节码文件加载到JVM
- 创建代理类实例对象,执行对象的目标方法
(2)cglib代理技术
CGLIB代理技术对类生成代理对象,被代理的类是否实现接口,无所谓CGLIB底层是采用类继承的方式,实现增强。
- 为类生成代理对象,被代理类有没有接口都无所谓,底层是生成子类,继承被代理类。
三、Spring的AOP技术--配置文件方式
1、AOP相关术语
(1)Joinpoint(连接点)
类里面有哪些方法可以增强这些方法称为连接点。
(2)Pointcut(切入点)
所谓切入点是指我们要对哪些Joinpoint进行拦截的定义,实际被增强的方法就是切入点。
(3)Advice(通知/增强)
所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知分为前置通知、后置通知、异常通知、最终通知、环绕通知(切面要完成的功能)。
- 前置通知:在一个方法之前执行
- 后置通知:在一个方法执行完之后执行
- 环绕通知:在一个方法执行之前和之后都执行
- 异常通知:当一个方法发生异常的时候执行
- 最终通知:类似于finally,在最后永远执行
(4)Aspect(切面)
是一个动作,将通知应用到切入点的过程,可以理解为是切入点+通知的结合,以后自己来编写和配置的。
2、基本准备工作
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,AspectJ实际上是对AOP编程思想的一个实践。
3、AOP配置文件方式的入门
创建maven项目,导入坐标依赖。
<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.0.2.RELEASE</version></dependency><dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>5.0.2.RELEASE</version></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.12</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><!--AOP联盟--><dependency><groupId>aopalliance</groupId><artifactId>aopalliance</artifactId><version>1.0</version></dependency><!--Spring Aspects--><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>5.0.2.RELEASE</version></dependency><!--aspectj--><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.8.3</version></dependency>
</dependencies>
下面,我们以登录为例,模拟实现登录操作,来更好理解AOP的相关术语。
这是一个Login类,其中包含实现基本登录功能的方法
public class Login {public void login(){//第一种方案,在这里添加增强功能的代码(不可行)System.out.println("登录成功......");}//现在我想在登录之前进行一次权限验证
}
现在我们想在登录之前进行一次权限验证,两种方法:
- 修改源代码,实现权限验证功能(这是不可行的)
- 不修改源代码,而是采用切入的方式
接着,我们创建切面类 ,也就是权限验证类
public class Authorization {public void anthorization(){System.out.println("进行了权限验证......");}
}
将目标类和切面类配置到Spring中
<bean id="login" class="com.qcby.Login"></bean>
<bean id="anth" class="com.qcby.Authorization"></bean>
在配置文件中完成AOP的配置
<!--配置切面--><aop:config><!--配置切面 = 切入点 + 通知组成--><aop:aspect ref="anth"><!--前置通知:UserServiceImpl的save方法执行前,会增强--><!--pointcut:后边是切入点表达式,作用是知道对对面的那个方法进行增强--><aop:before method="anthorization" pointcut="execution(public void com.qcby.Login.login())"/></aop:aspect></aop:config>
完成测试
import com.qcby.Login;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class LoginTest {@Testpublic void test(){ApplicationContext ac = new ClassPathXmlApplicationContext("Spring.xml");Login log = (Login) ac.getBean("login");log.login();}
}
测试结果如图,我们发现在登录成功之前,的确完成了权限验证。
4、切入点的表达式
在配置切入点的时候,需要定义表达式,具体展开如下:
切入点表达式的格式如下:
execution([修饰符] [返回值类型] [类全路径] [方法名 ( [参数] )])
- 修饰符可以省略不写,不是必须要出现的。
- 返回值类型是不能省略不写的,根据你的方法来编写返回值,可以使用 * 代替。
举例1:com.qcby.demo3.BookDaoImpl.save()
首先包名,类名,方法名是不能省略不写的,但是可以使用 * 代替。中间的包名可以使用 * 号代替。类名也可以使用 * 号代替,也有类似的写法:*DaoImpl。方法也可以使用 * 号代替。参数如果是一个参数可以使用 * 号代替,如果想代表任意参数使用 ..,比较通用的表达式:execution(* com.qcby.*.ServiceImpl.save(..))
举例2:com.qcby.demo3.BookDaoImpl--当中所有的方法进行增强
execution(* com.qcby.*.ServiceImpl.*(..))
举例3:com.qcby.demo3--包当中所有的方法进行增强
execution(* com.qcby.*.*.*(..))
5、AOP的通知类型
(1)前置通知:目标方法执行前进行增强。
如上述配置示例就是前置通知。
(2)环绕通知:目标方法执行前后,都可以进行增强。
想要进行环绕通知,就必须要在目标对象的方法进行手动配置。
public class Authorization {public void anthorization(ProceedingJoinPoint proceedingJoinPoint)throws Throwable{System.out.println("在方法执行之前,进行了权限验证......");proceedingJoinPoint.proceed();System.out.println("在方法执行之后,进行了权限验证......");}
}
将上述示例中xml配置方式改为:
<aop:around method="anthorization" pointcut="execution(public void com.qcby.Login.login())"/>
执行测试,可以发现在登录成功的前后都进行了权限验证。
(3)最终通知:目标方法执行成功或失败,进行增强。
将上述示例中xml配置方式改为:
<aop:after method="anthorization" pointcut="execution(public void com.qcby.Login.login())"/>
执行测试,可以发现权限验证发生在登录成功之后。
(4)后置通知:目标方法执行成功后,进行增强。
将上述示例中xml配置方式改为:
<aop:after-returning method="anthorization" pointcut="execution(public void com.qcby.Login.login())"/>
执行测试,可以发现权限验证发生在登录成功之后。
后置通知和最终通知的区别:
- 后置通知:切入点只有执行成功后才会执行。
- 最终通知:无论我们的切入点是否成功执行都会进行通知。
(5)异常通知:目标方法执行失败后,进行增强。(发生异常的时候才会执行,否则不执行)
将上述示例中xml配置方式改为:
<aop:after-throwing method="anthorization" pointcut="execution(public void com.qcby.Login.login())"/>
执行测试,可以发现仅执行了登录成功,而没有进行权限验证。 这是因为源代码中并无错误。
此时,如果源代码出现了异常:
public class Login {public void login(){//第一种方案,在这里添加增强功能的代码(不可行)int a = 10/0;System.out.println("登录成功......");}//现在我想在登录之前进行一次权限验证
}
那么执行结果将会进行权限验证,这就是异常通知:只有在切入点出现异常的时候才会进行通知。
四、Spring的AOP技术--注解方式
1、AOP注解方式入门程序
创建maven工程,导入坐标。编写接口,完成IOC的操作。步骤略。
编写切面类,给切面类添加注解 @Aspect,编写增强的方法,使用通知类型注解声明。
(1)配置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:context="http://www.springframework.org/schema/context"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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--开启注解扫描--><context:component-scan base-package="com.aopImpl"></context:component-scan></beans>
(2) 配置注解
import org.springframework.stereotype.Component;@Component
public class Login {public void login(){System.out.println("登录成功......");}
}
给切面类添加注解@Aspect,编写增强的方法,使用通知类型注解声明
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;@Component
@Aspect
public class Authorization {}
(3) 配置文件中开启自动代理
<?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:context="http://www.springframework.org/schema/context"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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--开启注解扫描--><context:component-scan base-package="com.aopImpl"></context:component-scan><!--开启Aspect生成代理对象--><aop:aspectj-autoproxy></aop:aspectj-autoproxy></beans>
(4)通知类型注解
@Component
@Aspect //生成代理对象
public class UserProxy {//增强/通知 ---》前置通知@Before(value = "execution(* com.*.User.add(..))")public void before(){System.out.println("before.............");}// 环绕通知@Around(value = "execution(* com.*.User.add(..))")public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {System.out.println("before.............");// 执行被增强的方法proceedingJoinPoint.proceed();System.out.println("after.............");}// 最终通知@After(value = "execution(* com.*.User.add(..))")public void after() {System.out.println("after.............");}//异常通知@AfterThrowing(value = "execution(* com.*.User.add(..))")public void afterThrowing() {System.out.println("afterThrowing.............");}//后置通知@AfterReturning(value = "execution(* com.*.User.add(..))")public void afterReturning() {System.out.println("afterReturning.............");}
}
(5) 测试类
@Test
public void aopTest1(){ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");user.add();
}