关于AOP

面向切面编程(Aspect-oriented Programming,俗称AOP)提供了一种面向对象编程(Object-oriented Programming,俗称OOP)的补充,面向对象编程最核心的单元是类(class),然而面向切面编程最核心的单元是切面(Aspects)。与面向对象的顺序流程不同,AOP采用的是横向切面的方式,注入与主业务流程无关的功能,例如事务管理和日志管理。

img

Spring的一个关键组件是AOP框架。 虽然Spring IoC容器不依赖于AOP(意味着你不需要在IOC中依赖AOP),但AOP为Spring IoC提供了非常强大的中间件解决方案。

AOP 是一种编程范式,最早由 AOP 联盟的组织提出的,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。它是 OOP的延续。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

我们之间的开发流程都是使用顺序流程,那么使用 AOP 之后,你就可以横向抽取重复代码,什么叫横向抽取呢?或许下面这幅图你能理解,先来看一下传统的软件开发存在什么样风险。

纵向继承体系

img

在改进方案之前,我们或许都遇到过 IDEA 对你输出 Duplicate Code 的时候,这个时候的类的设计是很糟糕的,代码写的也很冗余,基本上 if…else… 完成所有事情,这个时候就需要把相同的代码抽取出来成为公共的方法,降低耦合性。这种提取代码的方式是纵向抽取,纵向抽取的代码之间的关联关系非常密切。

横向抽取也是代码提取的一种方式,不过这种方式不会修改主要业务逻辑代码,只是在此基础上添加一些与主要的业务逻辑无关的功能,AOP 采取横向抽取机制,补充了传统纵向继承体系(OOP)无法解决的重复性 代码优化(性能监视、事务管理、安全检查、缓存),将业务逻辑和系统处理的代码(关闭连接、事务管理、操作日志记录)解耦。

AOP的作用

作用:在不修改源代码的情况下,可以实现功能的增强

传统的纵向体系代码复用:

这里写图片描述

横向抽取机制(AOP思想)

这里写图片描述

AOP 思想: 基于代理思想,对原来目标对象,创建代理对象,在不修改原对象代码情况下,通过代理对象,调用增强功能的代码,从而对原有业务方法进行增强 !

AOP应用场景

  • 记录日志
    • 场景二: 监控方法运行时间 (监控性能)
    • 场景三: 权限控制
    • 场景四: 缓存优化 (第一次调用查询数据库,将查询结果放入内存对象, 第二次调用, 直接从内存对象返回,不需要查询数据库 )
    • 场景五: 事务管理 (调用方法前开启事务, 调用方法后提交关闭事务 )

AOP的实现原理

那Spring中AOP是怎么实现的呢?Spring中AOP的有两种实现方式:

  • JDK动态代理
  • Cglib动态代理

JDK动态代理

  • 引入依赖,有spring,单元测,日志管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <dependencies>
    <!-- Spring -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    </dependency>

    <!-- 单元测试 -->
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
    </dependency>
    <!-- 日志 -->
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    </dependency>
    </dependencies
  • UserDao接口

    1
    2
    3
    public interface UserDao{
    public void saveUser();
    }
  • UserDao实现类

    1
    2
    3
    4
    5
    6
    public class userDaoImp implements UserDao {
    @Override
    public void saveUser(){
    System.out.println("持久层:用户保存");
    }
    }
  • 动态代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Test
    public void test(){
    final UserDao userDao = new UserDaoImpl();
    // newProxyInstance的三个参数解释
    // 参数1: 代理类加载器,同目标类的类加载器
    // 参数2: 代理类要实现的接口列表,同目标实现的接口列表
    // 参数3:回调,是一个InvocationHandler接口的实现对象,当调用代理对象的方法时,执行的是回调中的invoke方法
    // proxy为代理对象
    UserDao proxy = (UserDao) Proxy.newProxyInstance(
    userDao.getClass().getClassLoader(),
    userDao.getClass().getInterfaces(),
    new InvocationHandler() {

    @Override
    // 参数proxy: 被代理的对象
    // 参数method: 执行的方法,代理对象执行那个方法,method就是那个方法
    // 参数args: 执行方法的参数
    public Object invoke(Object proxy, Method method, Object[] args ) throws Throwable{
    System.out.println("记录日志");
    Object result = method.invoke(userDao, args);
    return result;
    }
    }
    );
    //代理对象执行方法
    proxy.saveUser();
    }
  • 结果

    在没有修改原来类的代码的情况下,对原有类的功能进行了增强

这里写图片描述

Cglib动态代理

在实际开发中,可能需要对没有实现接口的类增强,用JDK动态代理的方式就没法实现,采用CGlib动态代理可能对没有实现接口的类产生代理,实际上是生成了目标类的子类来增强

首先,需要导入Cglib所需的jar包。

  • 创建LinkManDao类,没有实现任何接口

    1
    2
    3
    4
    5
    public class LinkManDao{
    public void save(){
    System.out.println("持久层:联系人保存。。。。");
    }
    }
  • 动态代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Test
    public void test(){
    final LinkManDao linkManDao = new LinkManDao();
    // 创建cglib核心对象
    Enhancer enhancer = new Enhancer();
    // 设置父类
    enhancer.setSuperclass(linkManDao.getClass());
    // 设置回调
    enhancer.setCallback(new MethodInterceptor(){
    /**
    * 当你调用目标方法时,实质上是调用该方法
    * proxy:代理对象
    * method: 目标方法
    * args: 目标方法的形参
    * methodProxy: 代理方法
    */
    @Override
    public Object intercept(Object proxy,
    Method method,
    Object[] args,
    MethodProxy methodProxy)throws Throwable
    System.out.println("记录日志");
    Object result = method.invoke(linkManDao, args);
    return result;
    });
    // 创建代理对象
    LinkManDao proxy = (LinkManDao) enhancer.create();
    proxy.save();
    }

    结果:

    这里写图片描述

应用:@Transactional 注解

@Transactional是spring中声明式事务管理的注解配置方式,相信这个注解的作用大家都很清楚。@Transactional注解可以帮助我们把事务开启、提交或者回滚的操作,通过aop的方式进行管理。

通过@Transactional注解就能让spring为我们管理事务,免去了重复的事务管理逻辑,减少对业务代码的侵入,使我们开发人员能够专注于业务层面开发。

image-20210917201024599

我们知道实现@Transactional原理是基于spring aop,aop又是动态代理模式的实现,通过对源码的阅读,总结出下面的步骤来了解实际中,在spring 是如何利用aop来实现@Transactional的功能的。

spring中声明式事务实现原理猜想

首先,对于spring中aop实现原理有了解的话,应该知道想要对一个方法进行代理的话,肯定需要定义切点。在@Transactional的实现中,同样如此,spring为我们定义了以 @Transactional 注解为植入点的切点,这样才能知道@Transactional注解标注的方法需要被代理。

有了切面定义之后,在spring的bean的初始化过程中,就需要对实例化的bean进行代理,并且生成代理对象。

生成代理对象的代理逻辑中,进行方法调用时,需要先获取切面逻辑,@Transactional注解的切面逻辑类似于@Around,在spring中是实现一种类似代理逻辑。

image-20210917201203887

@Transactional作用

首先是@Transactional,作用是定义代理植入点。我们知道代理对象创建的通过BeanPostProcessor的实现类AnnotationAwareAspectJAutoProxyCreatorpostProcessAfterInstantiation方法来实现个,如果需要进行代理,那么在这个方法就会返回一个代理对象给容器,同时判断植入点也是在这个方法中。

那么下面开始分析,在配置好注解驱动方式的事务管理之后,spring会在ioc容器创建一个BeanFactoryTransactionAttributeSourceAdvisor实例,这个实例可以看作是一个切点,在判断一个bean在初始化过程中是否需要创建代理对象,都需要验证一次BeanFactoryTransactionAttributeSourceAdvisor是否是适用这个bean的切点。如果是,就需要创建代理对象,并且把BeanFactoryTransactionAttributeSourceAdvisor实例注入到代理对象中。

前文我们知道在AopUtils#findAdvisorsThatCanApply中判断切面是否适用当前bean,可以在这个地方断点分析调用堆栈,AopUtils#findAdvisorsThatCanApply一致调用,最终通过以下代码判断是否适用切点。

  • AbstractFallbackTransactionAttributeSource#computeTransactionAttribute(Method method, Class<?> targetClass) 这里可以根据参数打上条件断点进行调试分析调用栈,targetClass就是目标class …一系列调用
  • 最终SpringTransactionAnnotationParser#parseTransactionAnnotation(java.lang.reflect.AnnotatedElement)
1
2
3
4
5
6
7
8
9
10
11
12
@Override
public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) {
//这里就是分析Method是否被@Transactional注解标注,有的话,不用说BeanFactoryTransactionAttributeSourceAdvisor适配当前bean,进行代理,并且注入切点
//BeanFactoryTransactionAttributeSourceAdvisor
AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ae, Transactional.class);
if (attributes != null) {
return parseTransactionAnnotation(attributes);
}
else {
return null;
}
}

上面就是判断是否需要根据@Transactional进行代理对象创建的判断过程。@Transactional的作用一个就是标识方法需要被代理,一个就是携带事务管理需要的一些属性信息。