Spring作为整个Java圈中,影响力最大的框架,其核心的思想就是2个:
- IoC控制反转
- AOP面向切面编程
我们已经在之前的章节内容中,详细分享了Spring IoC相关的知识和实战解析:
- 【Spring框架精讲1】一文搞懂Spring IoC控制反转(附实战代码详解)
- 【Spring框架精讲2】一文搞懂IoC容器实现(附实战代码详解)
- 【Spring框架精讲3】一文搞懂Spring IoC注解(附实战代码详解)
- 【Spring框架精讲4】一文搞懂Spring JavaConfig(附实战代码详解)
本期,我们来对Spring的另一个重大致念——AOP面向切面编程,进行展开和分享。
内容比较多,推荐先收藏,有需要时,可以随时找到查看。

一、AOP面向切面编程
AOP面向切面编程(Aspect Oriented Programming),是面向对象编程的延申,将一般的对象内容精准到切面这种类型。它是一种基于业务逻辑的编程思维,切面本质上是包裹在特定方法外的代码块。
当需要开发一个企业级应用时,会有许多情况需要在目标函数外侧包裹一段一样的代码,以实现诸如日志记录、用户权限认证、事务管理、性能测试等功能,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类有许多核心方法,本例中的getTarget、getSignature就是其中之一,详细内容可以到本文第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>标签下的命名空间变量是标准写法,照抄即可,注意需要引入beans、context、aop三个空间;
- id为kDao的bean,是来自我们在2.1中所写的KoderDao的业务类;
- id为kMethod的bean,是来自我们在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。
- 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面向切面编程就先分享这些。
本期的内容有许多,有需要的朋友可以收藏这篇文章,方便随时查看。

我是专注于开发领域的@老K玩代码,会持续生产关于如何学习编程语言的优质内容。
如果你想学习Java编程,或者想精进你的Java编程能力,可以关注我。
如果你对开发、编程有任何疑问或者有想了解的内容,而我暂时没有写到的,也欢迎随时来找我聊聊。

#头条创作挑战赛#


