【Spring框架精讲5】从业务角度,理解AOP面向切面编程(附代码)

Spring作为整个Java圈中,影响力最大的框架,其核心的思想就是2个:

  • IoC控制反转
  • AOP面向切面编程

我们已经在之前的章节内容中,详细分享了Spring IoC相关的知识和实战解析:

  • 【Spring框架精讲1】一文搞懂Spring IoC控制反转(附实战代码详解)
  • 【Spring框架精讲2】一文搞懂IoC容器实现(附实战代码详解)
  • 【Spring框架精讲3】一文搞懂Spring IoC注解(附实战代码详解)
  • 【Spring框架精讲4】一文搞懂Spring JavaConfig(附实战代码详解)

本期,我们来对Spring的另一个重大致念——AOP面向切面编程,进行展开和分享。

内容比较多,推荐先收藏,有需要时,可以随时找到查看。

【Spring框架精讲5】从业务角度,理解AOP面向切面编程(附代码)


一、AOP面向切面编程

AOP面向切面编程(Aspect Oriented Programming),是面向对象编程的延申,将一般的对象内容精准到切面这种类型。它是一种基于业务逻辑的编程思维,切面本质上是包裹在特定方法外的代码块

当需要开发一个企业级应用时,会有许多情况需要在目标函数外侧包裹一段一样的代码,以实现诸如日志记录、用户权限认证、事务管理、性能测试等功能,AOP面向切面编程就是为此而生的。

【Spring框架精讲5】从业务角度,理解AOP面向切面编程(附代码)

此为图例,对于应用中的所有功能,我们每次调用都需要进行权限验证的场景,如果在每个功能方法上都写一遍验证逻辑是很不合理的,这时候我们引入一个验证用户权限的切面方法,就能很好地解决这个问题了。

面向切面编程的优势是显而易见的。

它的优点主要有以下几处:

  • 低耦合:可以有效降低切面的横向关注点与业务逻辑之间的依赖关系,降低耦合度;
  • 横向抽取:通过横向切割,将横向关注点与业务逻辑分离,提高代码的复用性和可维护性;
  • 透明性:AOP使主业务逻辑透明,切面逻辑不会对主业务逻辑造成干扰;

二、代码实例

2.1 前期准备工作

第一,我们在IDEA创建一个Maven项目,在pom.xml中引入下述依赖库坐标:

  • pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
</dependencies>
  • spring-context是Spring的核心框架;
  • aspectjwearver是Spring-AOP模块的外部依赖;

然后我们需要一个功能模块,我们将它写在KoderDao.java中:

  • KoderDao.java
public class KoderDao {
    public void insert() {
        System.out.println("数据插入成功");
    }
}
  • 代码超级简单,就是设计一个”插入数据“的演示函数;

2.2 切面类

然后我们编写切面类的代码:

  • KoderAspect.java
import org.aspectj.lang.JoinPoint;

public class KoderAspect {
    public void printMethod(JoinPoint joinPoint) {
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        System.out.println(className + "." + methodName);
    }
}
  • aspectj中引入JoinPoint类,是被切面函数拦截的点,具体实务中指向被增强的方法或代码片段;
  • JoinPoint类有许多核心方法,本例中的getTargetgetSignature就是其中之一,详细内容可以到本文第3节-JoinPoint核心方法查看;
  • 通过JoinPoint获得到对应的类和方法后,用getName获取对应类与方法的名字,即可获得Method的完整名称。

2.3 xml配置

在resources文档下新建xml配置文件:

  • 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: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"
>
    <bean id="kDao" class="KoderDao"/>
    <bean id="kMethod" class="KoderAspect"/>
    <aop:config>
        <aop:pointcut id="pointcut" expression="execution(public * *())"/>
        <aop:aspect ref="kMethod">
            <aop:before method="printMethod" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
</beans>
  • <beans>标签下的命名空间变量是标准写法,照抄即可,注意需要引入beanscontextaop三个空间;
  • idkDaobean,是来自我们在2.1中所写的KoderDao的业务类;
  • idkMethodbean,是来自我们在2.2中所写的KoderMethod的切面类;
  • 具体的切面配置,全部放在<aop:config>标签下;
  • <aop:pointcut>用于配置切点位置,其中expression属性中为切点表达式,会在本文第4节-切点表达式一节中详述;
  • <aop:aspect>是定义切面的标签,ref属性是定义切面bean,<aop:before>是定义切面的通知类型;
  • <aop:before>表明此处使用的是前置通知类型,常见的通知类型有5类,可在本文第5节-通知类型中查看完整信息;
  • <aop:before>中的method属性用于定义执行方法,名称要与切面类中的方法名一致;
  • <aop:before>中的pointcut-ref属性用于定义切点位置,名称要与此前定义的pointcut名称一致。

2.4 运行程序

完成上述所有配置,我们编写一个Application类,执行代码:

  • Application.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        KoderDao kDao = context.getBean("kDao", KoderDao.class);
        kDao.insert();
    }
}
  • 该类的编写是基本操作,不作展开

运行代码,得到以下结果:

KoderDao.insert
数据插入成功
  • 可以看到,切面类中的printMethod方法在insert执行前输出了结果;
  • 通过调整xml配置、切面类、切面方法,可以在业务代码执行前,实现诸如日志记录、权限验证等功能。

三、JoinPoint核心方法

JoinPoint是AOP中的一个抽象概念,表明程序执行中被拦截的点。

具体实务中,它是程序中被增强的方法或代码片段。

JoinPoint常用的核心方法有以下几个:

  • getArgs(): 获取方法参数。
  • getSignature(): 获取方法签名,即被增强的方法本身。
  • getTarget(): 获取目标对象类。
  • getThis(): 获取目标对象实例。

四、切点表达式

xml配置文件中的<aop:pointcut>标签是用来设置切点的,这里会用到切点表达式。

切点表达式的写法有点类似正则表达式,但整体比正则表达式更简单,特殊符号中只有2个比较重大:

  • *:代表1个元素;
  • ..:代表任意多个同类元素;

此处的”元素”表明目录、类、方法、变量等

试举几例,请自行比较体会:

  • public * com.koder..*.*(..)表明com.koder目录下的所有类下的方法;
  • public * com.koder..*.*(*,*)表明com.koder目录下的所有类下需要传入2个参数的方法;
  • public String com.koder..*.*(..)表明com.koder目录下的所有类下返回类型为String的方法;
  • public * com.koder..*Dao.*(..)表明com.koder目录下的所有类名以Dao结尾的类(如KoderDao)下的方法。

五、通知类型

5.1 概念

<aop:aspect>标签下需要配置通知类型,上例中我们设置前置通知类型,引用的是<aop:before>标签。其它几种通知类型和标签见下表:

关键字

通知类型

说明

before

前置通知

切面方法在目标方法运行前运行

after

后置通知

切面方法在目标方法运行后运行

after-returning

返回后通知

切面方法在目标方法返回数据后运行

after-throwing

异常后通知

切面方法在目标方法抛出异常后运行

around

环绕通知

用于自定义通知执行时机,实际涵盖上述4种情况

5.2 示例

将原xml配置文件中的<aop:before>标签和切面文件KoderAspect.java替换成以下代码,查看结果有何不同:

  • after示例
# applicationContext.xml
<aop:after method="printMethod" pointcut-ref="pointcut"/>
  • after-returning示例
# applicationContext.xml
<aop:after-returning method="printMethod" pointcut-ref="pointcut" returning="ret"/>

# KoderAspect.java
import org.aspectj.lang.JoinPoint;
public class KoderAspect {
    public void printMethod(JoinPoint joinPoint, Object ret) {
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        System.out.println(className + "." + methodName+" = "+ret);
    }
}
  • after-throwing示例
# applicationContext.xml
<aop:after-throwing method="printMethod" pointcut-ref="pointcut" throwing="th"/>

# KoderAspect.java
import org.aspectj.lang.JoinPoint;
public class KoderAspect {
    public void printMethod(JoinPoint joinPoint, Throwable th) {
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        System.out.println(className + "." + methodName+" = "+ret);
    }
}

##### 说明 #####
after-throwing整体逻辑和after-returning一样,差异就在于处理异常还是处理返回值。
在代码执行抛出异常的场景,上述切面方法生效。
  • around示例
# applicationContext.xml
<aop:around method="printMethod" pointcut-ref="pointcut"/>

# KoderAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
public class KoderAspect {
    public void printMethod(ProceedingJoinPoint pjp) {
        String className = pjp.getTarget().getClass().getName();
        String methodName = pjp.getSignature().getName();
        System.out.println("前置通知:"+className + "." + methodName);
        try {
            Object ret = pjp.proceed();
            System.out.println("返回后通知:"+className + "." + methodName+" = "+ret);
        } catch (Throwable th) {
            System.out.println("异常后通知:"+className + "." + methodName+" = "+th);
        } finally {
            System.out.println("后置通知:"+className + "." + methodName);
        }
    }
}


##### 说明 #####
- 这里引用了JoinPoint的子类,ProceedingJoinPoint类专门用于around环绕通知;
- 通过pjp.proceed()方法,可以执行目标方法,并获得返回值;
- 由于可以通过pjp规范目标方法的执行时机,所以around环绕通知,实则可以实现上述4种通知中的全部场景。

六、AOP注解

就像IoC可以通过注解实现一样,AOP也有自己的注解方案。

接下来,我们对上例中的代码进行如下调整,完成通过注解配置AOP。

  1. KoderDao.java
import org.springframework.stereotype.Repository;

@Repository("kDao")
public class KoderDao {
    public void insert(){
        System.out.println("数据插入成功");
    }
}
  • KoderDao注解@Repository并赋id值为kDao,将KoderDao放入IoC容器;
  • KoderAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class KoderAspect {

    @Around("execution(public * *())")
    public void printMethod(ProceedingJoinPoint pjp) {
        String className = pjp.getTarget().getClass().getName();
        String methodName = pjp.getSignature().getName();
        System.out.println("前置通知:"+className + "." + methodName);
        try {
            Object ret = pjp.proceed();
            System.out.println("返回后通知:"+className + "." + methodName+" = "+ret);
        } catch (Throwable th) {
            System.out.println("异常后通知:"+className + "." + methodName+" = "+th);
        } finally {
            System.out.println("后置通知:"+className + "." + methodName);
        }
    }
}
  • KoderAspect通过@Component注解,放入IoC容器中;
  • KoderAspect注解@Aspect,声明其为切面类;
  • 通过给切面方法注解@Around,实现环绕通知的配置声明,其参数为切点表达式;
  • AOP方法注解除了@Around外,很容易联想到还有@Before@After@AfterReturning@AfterThrowing这4个;
  • 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: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="/"/>
    <aop:aspectj-autoproxy/>
</beans>
  • <context:component-scan>Spring注解的声明标签;
  • base-package此处的参数值为/表明根目录,实际情况可根据自己项目结构进行调整;
  • <aop:aspectj-autoproxy/>是切面模块注解的生效声明。

结尾

好了,关于AOP面向切面编程就先分享这些。

本期的内容有许多,有需要的朋友可以收藏这篇文章,方便随时查看。

【Spring框架精讲5】从业务角度,理解AOP面向切面编程(附代码)

我是专注于开发领域的@老K玩代码,会持续生产关于如何学习编程语言的优质内容。

如果你想学习Java编程,或者想精进你的Java编程能力,可以关注我。

如果你对开发、编程有任何疑问或者有想了解的内容,而我暂时没有写到的,也欢迎随时来找我聊聊。

【Spring框架精讲5】从业务角度,理解AOP面向切面编程(附代码)

#头条创作挑战赛#

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...