四、AOP
4.1 基本概念
概念: AOP(Aspect Oriented Programming),意为:面向切面(方面)编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
优点: 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要意图:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码(即不通过修改源代码方式,在主干功能里面添加新功能)。
例子:
4.2 底层原理
AOP 底层使用动态代理,有两种情况动态代理:
- 第一种 有接口情况,使用 JDK 动态代理 创建接口实现类的代理对象,增强类的方法
- 第二种 没有接口情况,使用 CGLIB 动态代理 创建子类的代理对象,增强类的方法
4.3 JDK动态代理
使用 JDK 动态代理,使用 Proxy 类里面的方法创建代理对象
Proxy类:
创建对象使用的方法:
方法有三个参数: 第一参数,类加载器 第二参数,增强方法的所在类,这个类实现的接口(支持多个接口),也可以理解成代理类需要实现的接口 第三参数,实现 InvocationHandler 这个接口,创建代理对象,写增强的部分
实例:
①创建接口,定义方法
public interface UserService {
public void add();
public int update(int a,int b);
}
②创建接口实现类,实现方法
public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("UserServiceImpl的add方法执行了...");
}
@Override
public int update(int a, int b) {
System.out.println("UserServiceImpl的update方法执行了...");
return a + b;
}
}
③创建代理对象类,实现InvocationHandler接口
//创建的一个UserService接口代理对象类
public class UserServiceProxy implements InvocationHandler {
//通过有参构造函数把需要被代理的对象传给代理类
//这儿使用的Object可以改为具体的被代理类的对象的类型,使用Object类型只是为了能达到可以代理多个对象的目的
private Object obj;
public UserServiceProxy(Object obj){
this.obj = obj;
}
//将原先功能需要增强的业务逻辑写在这里面
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//执行被增强方法之前
System.out.println("方法执行之前...");
System.out.println("执行的方法是:" + method.getName());
System.out.println("传递的参数是:" + Arrays.toString(args));
//执行被增强的方法
Object invoke = method.invoke(obj, args); //底层的方法调用,这个方法可以在java基础的反射那一章节查看具体的,或在java.lang.reflect包中的Method类中查看该方法的具体讲解
//执行被增强方法之后
System.out.println("方法执行之后...");
System.out.println("该方法返回的值:" + invoke);
System.out.println("执行该方法的对象是:" + obj);
//System.out.println(proxy); 为什么这儿我想输出proxy会让程序一直执行导致堆栈溢出异常,希望如果有大佬仔细看见了能帮我解答一下
return invoke;
}
//InvocationHandler接口的invoke方法介绍:(个人根据文档的简易理解)
//proxy - 调用该方法的代理实例
//method - 使用代理对象调用的方法
//args - 代理对象调用的方法所需要的参数阵列
//返回结果 - 被调用方法的返回值
//返回结果原文介绍:从代理实例上的方法调用返回的值。如果接口方法的声明返回类型是原始类型,则此方法返回的值必须是对应的基本包装类的实例;
// 否则,它必须是可声明返回类型的类型。 如果此方法返回的值是null和接口方法的返回类型是基本类型,那么NullPointerException将由代理实例的方法调用抛出。
// 如上所述,如果此方法返回的值,否则不会与接口方法的声明的返回类型兼容,一个ClassCastException将代理实例的方法调用将抛出。
}
④使用Proxy类创建接口代理对象
public class JDKProxy {
public static void main(String[] args) {
//创建接口实现类的代理对象
Class[] interfaces = {UserService.class}; //创建出的代理对象需要实现的接口类型
//1、通过创建InvocationHandler匿名实现类对象来创建UserService接口实现类的代理对象的方式
//Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new InvocationHandler() {
// @Override
// public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// return null;
// }
//});
//2、通过创建InvocationHandler实现类的对象来创建UserService接口实现类的代理对象的方式
UserServiceImpl userServiceImplProxied = new UserServiceImpl(); //创建接口实现类对象(被代理类对象)
System.out.println("被代理的接口实现类对象:" + userServiceImplProxied);
//创建出UserService接口实现类UserServiceImpl的代理对象
UserService userServiceImplProxy = (UserService) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserServiceProxy(userServiceImplProxied));
userServiceImplProxy.add(); //创建出的代理对象调用被代理对象的方法
//执行结果:
//被代理的接口实现类对象:com.spring5.UserServiceImpl@61bbe9ba
//方法执行之前...
//执行的方法是:add
//传递的参数是:null
//UserServiceImpl的add方法执行了...
//方法执行之后...
//该方法返回的值:null
//执行该方法的对象是:com.spring5.UserServiceImpl@61bbe9ba
}
}
4.4 操作术语
AOP中有四个常用的操作术语,分别是:
- 连接点
- 一个类中可以被增强的方法就叫连接点
- 切入点
- 实际被增强的方法叫切入点
- 通知(增强)
- 实际增强的逻辑代码部分称为通知
- 通知的几种类型:
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
- 切面
- 是一个动作,即把通知应用到切入点的过程叫切面
4.5 AOP操作
4.5.1 准备工作
1、Spring 框架一般都是基于 AspectJ 实现 AOP 操作
- AspectJ 不是 Spring 组成部分,独立 AOP 框架,一般把 AspectJ 和 Spirng 框架一起使用,进行 AOP 操作
2、基于 AspectJ 实现 AOP 操作
- 基于 xml 配置文件实现
- 基于注解方式实现(通常使用)
3、在项目工程里面引入 AOP 相关依赖
<!--maven导入-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.15</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.16</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.8</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cglib</groupId>
<artifactId>com.springsource.net.sf.cglib</artifactId>
<version>2.2.0</version>
</dependency>
4、切入点表达式
- 切入点表达式作用:知道对哪个类里面的哪个方法进行增强
- 语法结构:
execution([权限修饰符] [返回类型] [类全路径] [方法名称]([参数列表]))
- 举例 1:对 com.spring5.dao.BookDao 类里面的 add 方法进行增强
execution(* com.spring5.dao.BookDao.add(..))
- 举例 2:对 com.spring5.dao.BookDao 类里面的所有的方法进行增强
execution(* com.spring5.dao.BookDao.* (..))
- 举例 3:对 com.spring5.dao 包里面所有类中的所有方法进行增强
execution(* com.atguigu.dao.*.* (..))
4.5.2 基于aspectj的注解方式
1.注解方式实现AOP
1、创建类,在类里面定义方法
public class User {
public void add(){
System.out.println("user add...");
}
}
2、创建增强类(编写增强逻辑),在增强类里面,创建方法,让不同方法代表不同通知类型
//代理类
public class UserProxy {
public void before(){
System.out.println("前置通知...");
}
}
3、进行通知的配置 ①在spring配置文件中,开启注解扫描(也可用注解方式开启注解扫描)
//注解方式
@Configuration
@ComponentScan(basePackages = {"com.spring5.aopannotation"})
public class SpringConfig {
}
<!--配置文件方式-->
<?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.spring5.aopannotation"></context:component-scan>
</beans>
②使用注解创建User和 UserProxy对象
@Component
public class User {
public void add(){
System.out.println("user add...");
}
}
@Component
public class UserProxy {
public void before(){
System.out.println("前置通知...");
}
}
③在增强(代理)类上面添加注解@Aspect
@Component
@Aspect
public class UserProxy {
public void before(){
System.out.println("前置通知...");
}
}
④在spring配置文件中开启生成代理对象
<?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.spring5.aopannotation"/>
<!-- 开启 Aspect 生成代理对象-->
<aop:aspectj-autoproxy/>
</beans>
4、配置不同类型的通知,在增强类中作为通知方法的上面添加通知类型注解,使用切入点表达式配置
@Component
@Aspect
public class UserProxy {
//前置通知
//@Before 注解表示作为前置通知
@Before("execution(* com.spring5.aopannotation.User.add())")
public void before(){
System.out.println("before...");
}
//最终通知——不管执行过程中有没有异常都会执行
@After("execution(* com.spring5.aopannotation.User.add())")
public void after(){
System.out.println("after...");
}
//后置通知(返回通知)——执行过程中出现异常就不执行
@AfterReturning("execution(* com.spring5.aopannotation.User.add())")
public void afterReturning(){
System.out.println("afterReturning...");
}
//异常通知
@AfterThrowing("execution(* com.spring5.aopannotation.User.add())")
public void afterThrowing(){
System.out.println("afterThrowing...");
}
//环绕通知
@Around("execution(* com.spring5.aopannotation.User.add())")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕之前...");
//执行被增强的方法
proceedingJoinPoint.proceed();
System.out.println("环绕之后...");
}
}
5、测试
public class TestSpring5 {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
User user = context.getBean("user", User.class);
user.add();
//环绕之前...
//before...
//user add...
//afterReturning...
//after...
//环绕之后...
//后面三个可能由于Spring版本原因输出结果不一样,弹幕中好像有人说从5.3.7开始底层就改变了
}
}
//另外,在Around通知的方法中如果使用try-catch处理异常的话当代码执行出现异常时“环绕之后”仍会执行输出,但如果使用throws的方式处理异常的话当代码出现异常时“环绕之后”则不会执行输出
2.提取公共切入点
@Pointcut("execution(* com.spring5.aopannotation.User.add())")
public void pointDemo(){
}
//前置通知
//@Before 注解表示作为前置通知
@Before("pointDemo()")
public void before(){
System.out.println("before...");
}
//最终通知
@After("pointDemo()")
public void after(){
System.out.println("after...");
}
3.多个增强类的优先级
当有多个增强类对同一个方法进行增强时,可以通过在增强类上添加@Order(数字类型值)
注解设置增强类的优先级,数字类型值越小优先级越高
//PersonProxy增强类
@Component
@Aspect
@Order(1)
public class PersonProxy {
@Pointcut("execution(* com.spring5.aopannotation.User.add())")
public void pointDemo(){
}
//前置通知
@Before("pointDemo()")
public void before(){
System.out.println("personProxy before...");
}
//最终通知
@After("pointDemo()")
public void after(){
System.out.println("personProxy after...");
}
}
//UserProxy增强类
@Component
@Aspect
@Order(3)
public class UserProxy {
@Pointcut("execution(* com.spring5.aopannotation.User.add())")
public void pointDemo(){
}
//前置通知
//@Before 注解表示作为前置通知
@Before("pointDemo()")
public void before(){
System.out.println("userProxy before...");
}
//最终通知
@After("pointDemo()")
public void after(){
System.out.println("userProxy after...");
}
}
//测试
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
User user = context.getBean("user", User.class);
user.add();
//personProxy before...
//userProxy before...
//user add...
//userProxy after...
//personProxy after...
}
4.完全注解开发
//创建配置类,不需要创建 xml 配置文件
@Configuration
@ComponentScan(basePackages = {"com.spring5.aopannotation"})
@EnableAspectJAutoProxy(proxyTargetClass = true) //开启Aspect生成代理对象
public class ConfigAop {
}
4.5.3 基于aspectj的xml配置文件方式
1、创建两个类,增强类和被增强类,创建方法
public class Book {
public void buy(){
System.out.println("book buy...");
}
}
public class BookProxy {
public void before() {
System.out.println("bookProxy before...");
}
}
2、在 spring 配置文件中创建两个类对象
<!--创建对象-->
<bean id="book" class="com.spring5.aopxml.Book"/>
<bean id="bookProxy" class="com.spring5.aopxml.BookProxy"/>
3、在 spring 配置文件中配置切入点
<!--需要引入aop名称空间-->
<!--配置aop增强-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="proxied" expression="execution(* com.spring5.aopxml.Book.buy())"/>
<!--配置切面(切面)-->
<aop:aspect ref="bookProxy">
<!--增强作用在具体的方法上(通知)-->
<aop:before method="before" pointcut-ref="proxied"/>
</aop:aspect>
</aop:config>
测试
@Test
public void test1(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean2.xml");
Book book = context.getBean("book", Book.class);
book.buy();
//bookProxy before...
//book buy...
}