【Spring学习笔记 七】深入理解Spring AOP实现机制

简介: 【Spring学习笔记 七】深入理解Spring AOP实现机制

AOP是什么,正如我标题所言,AOP是一种编程范式,同OOP一样,只是给出一种范式,具体的实现方式有多种多样,这一点需要明确,可以理解为一种思想模型和实现规范。

AOP范式

规范的定义是:AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

这个定义让我想起了之前的一篇Blog【Java Web编程 十】深入理解Servlet过滤器,在这篇Blog里我描述了Servlet过滤器的一些作用:用来拦截请求和过滤响应的,具体一点,主要用来完成一些通用的操作:登录检查(判断用户登录状态)、编码过滤、操作记录、事务管理、图像转换过滤、认证过滤、审核过滤、加密过滤等等,使用场景可能不同,但思想是较为通用的,就是执行一些公共操作,处理一些公共事务

Spring AOP理论基础

AOP是一种编程范式,那么Spring AOP就是一种具体的AOP实现方式,所以我们先来了解下Spring AOP的一些理论基础

Spring AOP基本概念

Spring AOP包含这样一些基本概念:引入、目标对象、连接点、切入点、AOP代理对象、横切关注点、切面、通知、织入等:

  • 引入(Introduction):声明额外的方法或字段。Spring AOP允许你向任何被通知(Advice)对象引入一个新的接口(及其实现类)。AOP允许在运行时动态的向代理对象实现新的接口来完成一些额外的功能并且不影响现有对象的功能1 定义代理接口,即规定一些实现类要实现的方法
  • 目标对象(Target object):被一个或多个切面(Aspect)所通知(Advice)的对象,也称作被通知对象。由于Spring AOP是通过运行时代理实现的,所以这个对象永远是被代理对象。所有的目标对象在AOP中都会生成一个代理类,AOP整个过程都是针对代理类在进行处理。2 定义真实实现类
  • 连接点(Join point):程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中一个连接点总是代表一个方法的执行AOP拦截到的方法就是一个连接点。通过声明一个org.aspectj.lang.JoinPoint类型参数我们可以在通知(Advice)中获得连接点的信息。2-1 实现类中的实现方法
  • AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能),在Spring中AOP可以是JDK动态代理或者是CGLIB代理。其实就是代理对象3 定义代理类或代理工厂
  • 横切关注点:跨越应用程序多个模块的方法或功能。即与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等,是一个概念
  • 切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是Java应用程序中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用通过类(基于模式(XML)的风格)或者在普通类中以@Aspect注解(AspectJ风格)来实现。3-1 定义一个处理相关模块的代理
  • 切入点(Pointcut):匹配连接点(Join point)的断言。通知(Advice)跟切入点表达式关联,并在与切入点匹配的任何连接点上面运行。切入点表达式如何跟连接点匹配是AOP的核心,Spring默认使用AspectJ作为切入点语法。通过切入点的表达式来确定哪些方法要被AOP拦截,之后这些被拦截的方法会执行相对应的Advice代码3-2 定义哪些实现方法要被处理,用来引导通知作用到连接点上
  • 通知(Advice):在切面(Aspect)的某个特定连接点上(Join point)执行的动作。通知的类型包括around,before,after等等。通知的类型将在后面进行讨论。许多AOP框架,包括Spring 都是以拦截器作为通知的模型,并维护一个以连接点为中心的拦截器链。总之就是AOP对连接点的处理通过通知来执行。Advice指当一个方法被AOP拦截到的时候要执行的代码3-3 额外的处理逻辑作用在切入点之上,通常也是方法
  • 织入(Weaving):把切面(aspect)连接到其他的应用程序类型或者对象上,并创建一个被通知对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯AOP框架一样,在运行时完成织入。实质上就是把切面跟对象关联并创建该对象的代理对象的过程4 动态生成代理类并执行方法

其实以上各个定义我故意将其与动态代理的实现步骤相对应,意在表明实际上Spring AOP就是动态代理的一种封装实现

通知Advice五种类型

上文提到的通知,其实按照作用点分为好几种,在动态代理的那篇Blog里我们只简单的描述了下方法执行前后,其实作用位置有好几种:

  • 前置通知(Before advice):在某个连接点(Join point)之前执行的通知,但这个通知不能阻止连接点的执行(除非它抛出一个异常)。
  • 后置通知(After(finally)advice):当某个连接点(Join point)退出的时候执行的通知(不论是正常返回还是发生异常退出)。
  • 返回后通知(After returning advice):在某个连接点(Join point)正常完成后执行的通知。例如,一个方法没有抛出任何异常正常返回。
  • 抛出异常后通知(After throwing advice):在方法抛出异常后执行的通知。
  • 环绕通知(Around advice):包围一个连接点(Join point)的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行

我们比较常用的可能是前置和后置通知。

切入点表达式

切入点描述了我们要对哪些连接点(方法)进行切入操作,所以切入点也会类似Spring包扫描一样,扫描我们要切入的方法,切入点使用espression表达式来限定实现范围。Spring AOP支持的AspectJ切入点指示符如下:

  • args:用于匹配当前执行的方法传入的参数为指定类型的执行方法
  • execution:用于匹配方法执行的连接点
  • within:用于匹配指定类型内的方法执行
  • this:用于匹配当前AOP代理对象类型的执行方法
  • target:用于匹配当前目标对象类型的执行方法

我们一般较为常用的就是execution

我们来了解下AspectJ类型匹配的通配符:

  • *:匹配任何数量字符
  • ..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
  • +:匹配指定类型的子类型;仅能作为后缀放在类型模式后边

示例如下:

java.lang.String    匹配String类型;  
java.*.String       匹配java包下的任何“一级子包”下的String类型;  如匹配java.lang.String,但不匹配java.lang.ss.String  
java..*             匹配java包及任何子包下的任何类型; 如匹配java.lang.String、java.lang.annotation.Annotation  
java.lang.*ing      匹配任何java.lang包下的以ing结尾的类型;  
java.lang.Number+   匹配java.lang包下的任何Number的自类型;如匹配java.lang.Integer,也匹配java.math.BigInteger

具体就不再展开,具体使用时再详细了解,大概有个认知即可。

Spring AOP实现机制

我们来具体看下Spring Aop的实现机制,准备好环境好看三种实现方式:原生动态代理,XML以及注解。

0 准备实现测试环境

我们需要提前配置好需要依赖的pom.xml,以及目标对象的接口和实现定义,AOP的切面Model定义,并把它们都注册到核心配置文件中:

1 引入AOP相关依赖

我们在pom.xml文件中增加AOP实现的所需相关依赖:

<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.9</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.3.9</version>
        </dependency>
        <!-- 使用AOP织入,需要导入一个依赖包 -->    
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>

2 配置引入接口

我们提前配置好目标对象,为了详细测试,我们定义两个,一个是AccountService,一个是UserService:

AccountService

package com.example.Spring.service;
public interface AccountService {
    /**
     * 引入实现的方法
     */
    void addAccount(String accountId);
}

UserService

package com.example.Spring.service;
/**
 * * @Name UserService
 * * @Description
 * * @author tianmaolin
 * * @Data 2021/8/24
 */
public interface UserService {
    /**
     * 引入实现的方法
     */
    void deleteUser(String userid);
}

3 配置目标对象

接下来我们配置相关的目标对象,这里的目标对象同之前的动态代理里的概念类似,由于JDK代理的实现方式是基于接口的,所以我们一个目标对象只能来源于一个接口,当然一个引入接口可以有多个目标对象的实现方式:

AccountServiceImpl

package com.example.Spring.serviceImpl;
import com.example.Spring.service.AccountService;
public class AccountServiceImpl implements AccountService {
    /**
     * 引入实现的方法
     *
     * @param accountId
     */
    @Override
    public void addAccount(String accountId) {
        System.out.println("新增账户成功,增加的账户id为"+accountId);
    }
}

UserServiceImpl

package com.example.Spring.serviceImpl;
import com.example.Spring.service.UserService;
public class UserServiceImpl implements UserService {
    /**
     * 引入实现的方法
     *
     * @param userid
     */
    @Override
    public void deleteUser(String userid) {
        System.out.println("删除用户成功,删掉的用户id为: "+userid);
    }
}

然后我们定义代理对象(也就是模块化的切面),我们想要在方法请求前关注是否有权限,以及方法请求前后关注日志记录:

LogAop

package com.example.spring.aop;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
 * * @Name LogProxy
 * * @Description
 * * @author tianmaolin
 * * @Data 2021/8/24
 */
public class LogAop implements MethodBeforeAdvice, AfterReturningAdvice {
    @Override
    public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
        System.out.println("日志记录开始"+arg2.getClass().getSimpleName()+"的"+arg0.getName()+"方法开始被执行"+ LocalDateTime.now());
    }
    @Override
    public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable {
        System.out.println("日志记录结束"+arg3.getClass().getSimpleName()+"的"+arg1.getName()+"方法被执行完成"+ LocalDateTime.now());
    }
    public void beforeLog()  {
        System.out.println("日志记录开始"+ LocalDateTime.now());
    }
    public void afterLog()  {
        System.out.println("日志记录结束"+ LocalDateTime.now().plusMinutes(5));
    }
}

PermissionAop

package com.example.spring.aop;
import org.aspectj.lang.annotation.Before;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
 * * @Name StudyProxy
 * * @Description
 * * @author tianmaolin
 * * @Data 2021/8/24
 */
public class PermissionAop implements MethodBeforeAdvice {
    @Override
    public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
        System.out.println("权限校验开始"+arg2.getClass().getSimpleName()+"的"+arg0.getName()+"方法进入前权限校验"+ LocalDateTime.now());
    }
    public void beforePermission()  {
        System.out.println("权限校验开始"+ LocalDateTime.now());
    }
}

1 基于Spring API 配置实现

基于XML的配置方式有两种,一种就是基于注解本身去实现,另一种是基于AOP的标签去实现。我们先来看原生动态代理的实现方式

使用的applicationContext.xml文件如下:

<!-- 定义目标对象:定义被代理者 -->
    <bean id="accountServiceImpl" class="com.example.Spring.serviceImpl.AccountServiceImpl"></bean>
    <bean id="userServiceImpl" class="com.example.Spring.serviceImpl.UserServiceImpl"></bean>
    <!-- 定义切面,切面内包含通知要执行的方法-->
    <bean id="permissionAop" class="com.example.Spring.aop.PermissionAop"></bean>
    <bean id="logAop" class="com.example.Spring.aop.LogAop"></bean>
    <!-- 定义切入点,描述切入点绑定的连接点 -->
    <bean id="permissionPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
        <property name="pattern" value=".*"></property>
    </bean>
     <!-- 定义切入点,描述切入点绑定的连接点 -->
    <bean id="logPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
        <property name="pattern" value=".*"></property>
    </bean>
    <!-- 完成切面配置,绑定切入点和通知 -->
    <bean id="permissionAdvisorRelPointCut" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="permissionAop"></property>
        <property name="pointcut" ref="accountPointcut"></property>
    </bean>
    <bean id="logAdvisorRelPointCut" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="logAop"></property>
        <property name="pointcut" ref="pointcut"></property>
    </bean>
    <!-- 织入:设置代理并执行-->
    <bean id="accountProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!-- 引入:   代理的接口 -->
        <property name="proxyInterfaces" value="com.example.Spring.service.AccountService"></property>
        <!-- 目标对象:代理的对象 -->
        <property name="target" ref="accountServiceImpl"></property>
        <!-- 代理对象:切面模块化配置 -->
        <property name="interceptorNames" value="permissionAdvisorRelPointCut"></property>
    </bean>

使用测试类进行测试:

@Test
    public void testByAopApi(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService accountService = (AccountService)applicationContext.getBean("accountProxy");
        accountService.addAccount("001");
    }

打印结果如下:

我们可以看到其实落地实现是接口,也就是说如果实现类accountServiceImpl也实现了别的接口,我们这里就可以强制转换为对应接口并实现对应接口方法,假如accountServiceImpl实现了UserService,那么也可以在这里强转为该接口并实现该接口的方法:deleteUser。可以说是很灵活的。

但是我们可以看到这里有一些限制,和动态注解一样,我们最后生成的代理对象实际上是基于某个接口:

  • 这个接口只绑定了一个AOP实现切面,例如我这里AccountService 绑定了permissionAop,要是还想实现LogAop相关的配置就比较难了,我们还需要创建一个绑定配置,并且再配置到一个切面配置中,类似于我们又使用了另一个代理工厂,也就是我们的AOP代理使用时只能用其中一个代理工厂,也就是AOP
  • 还有一个限制就是,如果我想给userServiceImpl实现AOP,那么我又需要另一套配置,如果想要使用两个代理LogAop和permissionAop, 需要的配置就是两套

基于以上的限制,我们的AOP配置岂不是一大坨?这些绑定关系这么复杂怎么办,这就需要自动AOP绑定,我们可以把上述织入部分的代码替换为:

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

通过这个自动AOP,Spring会扫描类class名,找到所有的切入点并且绑定上配置的AOP,这样每个目标对象都会绑定多个AOP,使用时就很方便。但是目前的写法不够清晰,我们从AOP的角度去使用标签来配置实现更好理解一些。

2 基于Spring AOP标签配置实现

这种方式首先需要引入AOP的相关约束,才能使用aop配置:

http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

使用的applicationContext.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:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 定义目标对象:定义被代理者 -->
    <bean id="personService" class="com.example.spring.serviceImpl.PersonServiceImpl"></bean>
    <bean id="userService" class="com.example.spring.serviceImpl.UserServiceImpl"></bean>
    <!-- 定义切面,切面内包含通知要执行的方法-->
    <bean id="permissionAop" class="com.example.spring.aop.PermissionAop"></bean>
    <bean id="logAop" class="com.example.spring.aop.LogAop"></bean>
      <!--aop的配置-->
    <aop:config>
        <!--切面配置-->
        <aop:aspect ref="logAop">
            <!--切点-->
            <aop:pointcut id="logPointCut" expression="execution(* com.example.Spring.service..*.*(..))"/>
            <!--切点-通知-->
            <aop:before pointcut-ref="logPointCut" method="beforeLog"/>
            <aop:after pointcut-ref="logPointCut" method="afterLog"/>
        </aop:aspect>
        <aop:aspect ref="permissionAop">
            <aop:pointcut id="permissionPointCut" expression="execution(* com.example.Spring.service..*.*(..))"/>
            <aop:before pointcut-ref="permissionPointCut" method="beforePermission"/>
        </aop:aspect>
    </aop:config>
</beans>

我们使用单元测试测试下实现:

@Test
    public void testByAopLabel(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        //落地还是落地到某个具体的接口上了,所以我们一般是一个接口对应一个实现类
        UserService userService = (UserService) applicationContext.getBean("userServiceImpl");
        userService.deleteUser("001");
        System.out.println("=======================================================================");
        AccountService accountService = (AccountService) applicationContext.getBean("accountServiceImpl");
        accountService.addAccount("005");
    }

打印结果如下:

可以看到我们在使用时完全感受不到强绑定的存在,这些都被AOP封装好了,我们在调用方法实现时就像在进行普通的实现一样,完全没有感知,因为AOP配置在扫描到切点后就自动附加了切面和通知

2 基于注解配置方式实现

我们知道Spring的注解有三种,基于XML,基于注解以及基于Java配置。我们再来增加一个基于注解实现的AOP类,在访问方法前进行参数校验。首先我们创建一个AOP:

ArgCheckAop

package com.example.Spring.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class ArgCheckAop {
    @Before(value="execution(* com.example.Spring.service.AccountService.addAccount(..)) && args(accountId)",argNames = "accountId")
    public void before(String accountId){
        System.out.println("---------方法执行前进行参数校验---------");
        if(Integer.parseInt(accountId)>10){
            System.out.println("--------accountId为"+accountId+"参数校验通过---------");
        }else{
            System.out.println("--------accountId为"+accountId+"参数校验不通过---------");
        }
    }
}

然后我们需要在配置文件中加入注解声明和新加入的AOP:

<!-- 使用aop注解 -->
 <aop:aspectj-autoproxy/>
 <bean id="argCheckAop" class="com.example.Spring.aop.ArgCheckAop"></bean>

加入测试类如下:

@Test
    public void testByAopAnnotation(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        //落地还是落地到某个具体的接口上了,所以我们一般是一个接口对应一个实现类
        UserService userService = (UserService) applicationContext.getBean("userServiceImpl");
        userService.deleteUser("001");
        System.out.println("=======================================================================");
        AccountService accountService = (AccountService) applicationContext.getBean("accountServiceImpl");
        accountService.addAccount("0012");
    }

打印结果如下:

通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配置@aspectJ切面的bean创建代理,织入切面。当然,spring 在内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现的细节已经被<aop:aspectj-autoproxy />隐藏起来了。

总结一下

本篇Blog讨论了基于动态代理的几种AOP的Spring的实现方式,包括原生动态代理方式、XML配置方式、注解方式,对于AOP来说还是注解方式用起来流畅一些,至此,Sping的两大核心思想IOC以及AOP都有了一个详细的认知了,有了AOP大多数通用操作都能被轻而易举的实现了,同时由于我们大多数使用场景一个接口对应一个实现类,所以相比于CGLIB的实现方式,我们更多使用JDKProxy,这种方式基于接口及对应方法,面向接口也更符合AOP横切的统一认知

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1天前
|
XML 监控 Java
Java一分钟之-Spring AOP:基于Spring的AOP
【6月更文挑战第13天】Spring框架集成AOP支持,便于实现如日志、监控和事务管理等关注点的集中管理。本文探讨Spring AOP的核心概念(切面、切入点、通知和代理),常见问题(代理对象理解不清、切入点表达式错误、通知类型混淆和事务管理配置不当)及其对策,并提供注解式日志记录代码示例。通过学习和实践,开发者能更好地运用Spring AOP提升代码质量。
14 2
|
11天前
|
Java Spring
【JavaEE进阶】 Spring AOP源码简单剖析
【JavaEE进阶】 Spring AOP源码简单剖析
|
11天前
|
Java Spring
【JavaEE进阶】 Spring AOP详解
【JavaEE进阶】 Spring AOP详解
|
11天前
|
数据采集 Java 程序员
【JavaEE进阶】 Spring AOP快速上手
【JavaEE进阶】 Spring AOP快速上手
|
16天前
|
Java Spring
|
17天前
|
Java Spring
|
17天前
|
前端开发 Java Maven
Spring AOP
Spring AOP
20 1
|
17天前
|
数据采集 XML 监控
Spring AOP
Spring AOP
37 2
|
22天前
|
Java Spring 容器
Spring AOP 代码案例
Spring AOP 代码案例
34 1
|
28天前
|
前端开发 Java 关系型数据库
使用IDEA搭建一个Spring + AOP (权限管理 ) + Spring MVC
使用IDEA搭建一个Spring + AOP (权限管理 ) + Spring MVC