详解Spring框架的AOP机制
AOP是Spring框架面向切面的编程思想,AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。以下结合实际案例详细讲述AOP的原理及实现过程。
目的:
- 理解AOP的编程思想及原理
- 掌握AOP的实现技术
Spring框架的AOP机制可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。
例如,在一个业务系统中,用户登录是基础功能,凡是涉及到用户的业务流程都要求用户进行系统登录。如果把用户登录功能代码写入到每个业务流程中,会造成代码冗余,维护也非常麻烦,当需要修改用户登录功能时,就需要修改每个业务流程的用户登录代码,这种处理方式显然是不可取的。比较好的做法是把用户登录功能抽取出来,形成独立的模块,当业务流程需要用户登录时,系统自动把登录功能切入到业务流程中。下图是用户登录功能切入到业务流程示意图。
1. 一个AOP案例描述
在案例SpringProgram项目中,一个业务流程是校长通过邮件发送上课通知给老师。校长执行该业务时,业务系统并没有对老师进行验证。现在要求校长在发送通知之前,需要对老师进行用户验证。
具体要求是在尽量不改变原有业务代码的情况下,加入老师验证功能。
使用EmailNoticeImpl业务类对老师身份进行验证,可以考虑在执行setTeacher方法之前执行验证功能。
具体的操作步骤是:
- 添加老师身份验证功能代码,用于切入到EmailNoticeImpl业务类,身份验证功能代码也称为切面,切入点为EmailNoticeImpl业务类的setTeacher方法;
- 在Spring配置文件中配置AOP,添加切入面、切入点以及需要切入的目标Bean;
- 编写测试代码,测试程序运行地正确性。
2. 实现AOP案例代码
在实现AOP案例之前,需要确定项目已经引入了Spring框架关于AOP功能的Jar包。下面列出的是spring-aop-5.0版本,其它版本也可以。
- spring-aop-5.0.8.RELEASE
- spring-aspects-5.0.8.RELEASE
另外还需要引入下面的Jar包:
-
aspectjrt
-
aspectjweaver
2.1 Teacher实体类
示例代码:
package com.echo.spring.springaop.entity;
public class Teacher {
private String name;
private String classTime;
public String getNotify() {
return name + "在" + classTime;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClassTime() {
return classTime;
}
public void setClassTime(String classTime) {
this.classTime = classTime;
}
@Override
public String toString() {
return "Teacher{" +
"name='" + name + '\'' +
", classTime='" + classTime + '\'' +
'}';
}
}
2.2 Principal实体类
示例代码:
package com.echo.spring.springaop.entity;
import com.echo.spring.springaop.service.NoticeService;
public class Principal {
public void notifyTeacher(Teacher teacher, NoticeService notice) {
notice.sendMessage(teacher);
String notify = teacher.getNotify();
System.out.println(notify);
}
}
2.3 添加老师身份验证功能
在案例中,添加VerifyUserAspect类,并添加类方法beforeAdvice,该方法需要传入Teacher实体类,用于验证老师身份的合法性。
package com.echo.spring.springaop.service.impl;
import com.echo.spring.springaop.entity.Teacher;
// 切面
public class VerifyUserAspect {
// 关注点
public void beforeAdvice(Teacher teacher) {
System.out.println(teacher.getName() + "验证成功!");
}
}
2.4 NoticeService接口
示例代码:
package com.echo.spring.springaop.service;
import com.echo.spring.springaop.entity.Teacher;
public interface NoticeService {
void sendMessage(Teacher teacher);
}
2.5 添加EmailNoticeImpl业务类
在案例中,添加EmailNoticeImpl业务类。
package com.echo.spring.springaop.service.impl;
import com.echo.spring.springaop.entity.Teacher;
import com.echo.spring.springaop.service.NoticeService;
public class EmailNoticeImpl implements NoticeService {
private Teacher teacher;
private String message;
@Override
public void sendMessage(Teacher teacher) {
teacher.setClassTime(message + " 邮件发送!");
}
public Teacher getTeacher() {
return teacher;
}
// 切入点
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
EmailNoticeImpl业务类内置了Teacher对象,并通过sendMessage方法发送通知给Teacher对象。setTeacher方法用于设置Teacher对象,在设置之前需要验证Teacher对象身份的合法性,也就是要在setTeacher方法执行之前,执行VerifyUserAspect类的beforeAdvice方法。
2.6 添加Spring配置文件
在案例中,添加Spring配置文件aop.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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
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">
<context:component-scan base-package="com.echo.spring.springaop"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>
<bean id="teacherZhang"
class="com.echo.spring.springaop.entity.Teacher"
P:name="张老师"/>
<bean id="emailNoticeZhang"
class="com.echo.spring.springaop.service.impl.EmailNoticeImpl"
P:teacher-ref="teacherZhang"
P:message="8:45上语文课"/>
<bean id="verifyUserAspect"
class="com.echo.spring.springaop.service.impl.VerifyUserAspect"/>
<aop:config>
<aop:pointcut id="verifyUser"
expression="execution(* com.echo.spring.springaop.service.impl.EmailNoticeImpl.sendMessage(..)) and args(teacher)"/>
<aop:aspect ref="verifyUserAspect">
<aop:before method="beforeAdvice" pointcut-ref="verifyUser" arg-names="teacher"/>
</aop:aspect>
</aop:config>
</beans>
aop.xml需要使用AOP命名空间,因此需要在配置文件中导入spring-aop架构,添加下面的AOP命名空间。
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
<aop:aspectj-autoproxy/>
标签用于AOP创建代理切入类(代理类)是基于接口的还是基于类的,如果<aop:aspectj-autoproxy/>
的proxy-target-class 属性值被设置为true,那么创建基于类的代理,否则创建基于接口的代理。
<aop:config>
标签用于配置AOP。其中子标签<aop:aspect >
添加一个切面Aspect,一个切面可以是一个模块或一个类。例如验证用户身份的VerifyUserAspect类,该切面将被AOP调用。可以配值多个切面。
<aop:pointcut>
标签用于声明一个切入点,也就是切面提供的哪些方法可以被调用。<aop:pointcut>
的expression
属性 为切入点的表达式,用于定义切入点的路径。
例如:
execution(* com.echo.spring.springaop.service.impl.EmailNoticeImpl.setTeacher(..))
上述表达式的意思是切入点为EmailNoticeImpl的setTeacher方法。其中*号表示切入点为任意的返回类型,(..)表示任意参数。
再如:
execution(* com.echo.spring.springaop. .*.*(..))
上述表达式的意思是切入点为com.echo.spring.springaop包及子包下所有的类及类中所有的方法。
又如:
execution(* com.echo.spring.springaop. *.*(..))
上述表达式的意思是切入点为com.echo.spring.springaop包下所有的类及类中所有的方法。
2.5 编写测试代码
在案例中,添加AopTest测试类。
package com.echo.spring.springaop;
import com.echo.spring.springaop.entity.Principal;
import com.echo.spring.springaop.entity.Teacher;
import com.echo.spring.springaop.service.impl.EmailNoticeImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AOPTest {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml");
EmailNoticeImpl emailNotice = (EmailNoticeImpl) ctx.getBean("emailNoticeZhang");
Teacher teacherZhang = (Teacher) ctx.getBean("teacherZhang");
Principal principal = new Principal();
principal.notifyTeacher(teacherZhang, emailNotice);
}
}
测试程序运行结果如下图所示。
3. AOP编程思想及术语
AOP是面向切面的编程,其编程思想是把散布于不同业务但功能相同的代码从业务逻辑中抽取出来,封装成独立的模块,这些独立的模块被称为切面,切面的具体功能方法被称为关注点。在业务逻辑执行过程中,AOP会把分离出来的切面和关注点动态切入到业务流程中,这样做的好处是提高了功能代码的重用性和可维护性。
例如,前面案例的VerifyUserAspect类就是切面,VerifyUserAspect类的beforeAdvice就是关注点。VerifyUser切面的功能就是验证老师身份,可以应用到与老师相关的不同业务流程中。EmailNoticeImpl是一个业务类,负责发送通知给老师,在发送通知之前需要验证老师的身份。AOP会在EmailNoticeImpl类的setTeacher方法执行之前,将beforeAdvice关注点切入到EmailNoticeImpl业务类中,并执行beforeAdvice方法。
Spring框架提供了@AspectJ 注解方法和基于XML架构的方法来实现AOP。前面的案例是基于XML架构的方法,后面一节会讲述基于@AspectJ 注解的方法。下面结合前面的案例讲述一下AOP的相关术语。
● Aspect
表示切面。切入业务流程的一个独立模块。例如,前面案例的VerifyUserAspect类,一个应用程序可以拥有任意数量的切面。
● Join point
表示连接点。也就是业务流程在运行过程中需要插入切面的具体位置。例如,前面案例的EmailNoticeImpl类的setTeacher方法就是一个连接点。
● Advice
表示通知。是切面的具体实现方法。可分为前置通知(@Before)、后置通知(@After)、异常通知(@AfterThrowing)、返回通知(@AfterReturning)和环绕通知(@Around)五种。实现方法具体属于哪类通知,是在配置文件和注解中指定的。例如,VerifyUserAspect类的beforeAdvice方法就是前置通知。
● Pointcut
表示切入点。用于定义通知应该切入到哪些连接点上,不同的通知通常需要切入到不同的连接点上。例如,前面案例配置文件的<aop:pointcut>
标签。
● Target
表示目标对象。被一个或者多个切面所通知的对象。例如,前面案例的EmailNoticeImpl类。
● Proxy
表示代理对象。将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象为目标对象的业务逻辑功能加上被切入的切面所形成的对象。
● Weaving
表示切入,也称为织入。将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期。
4. 总结
- 探讨了AOP的编程思想,其主要思想是让开发者把诸多业务流程中的通用功能抽取出来,单独编写功能代码,形成独立的模块,这些模块也被称为切面。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把切面切入到流程的合适位置。
- 通过案例讲述了AOP的实现过程,具体实现步骤是:首先编写需要切入业务流程的独立模块(也称为切面)和切入点(模块中的方法);然后在Spring配置文件中配置AOP,添加切入面、切入点以及需要切入的目标Bean;最后编写测试代码。
┆ 凉 ┆ 暖 ┆ 降 ┆ 等 ┆ 幸 ┆ 我 ┆ 我 ┆ 里 ┆ 将 ┆ ┆ 可 ┆ 有 ┆ 谦 ┆ 戮 ┆ 那 ┆ ┆ 大 ┆ ┆ 始 ┆ 然 ┆
┆ 薄 ┆ 一 ┆ 临 ┆ 你 ┆ 的 ┆ 还 ┆ 没 ┆ ┆ 来 ┆ ┆ 是 ┆ 来 ┆ 逊 ┆ 没 ┆ 些 ┆ ┆ 雁 ┆ ┆ 终 ┆ 而 ┆
┆ ┆ 暖 ┆ ┆ 如 ┆ 地 ┆ 站 ┆ 有 ┆ ┆ 也 ┆ ┆ 我 ┆ ┆ 的 ┆ 有 ┆ 精 ┆ ┆ 也 ┆ ┆ 没 ┆ 你 ┆
┆ ┆ 这 ┆ ┆ 试 ┆ 方 ┆ 在 ┆ 逃 ┆ ┆ 会 ┆ ┆ 在 ┆ ┆ 清 ┆ 来 ┆ 准 ┆ ┆ 没 ┆ ┆ 有 ┆ 没 ┆
┆ ┆ 生 ┆ ┆ 探 ┆ ┆ 最 ┆ 避 ┆ ┆ 在 ┆ ┆ 这 ┆ ┆ 晨 ┆ ┆ 的 ┆ ┆ 有 ┆ ┆ 来 ┆ 有 ┆
┆ ┆ 之 ┆ ┆ 般 ┆ ┆ 不 ┆ ┆ ┆ 这 ┆ ┆ 里 ┆ ┆ 没 ┆ ┆ 杀 ┆ ┆ 来 ┆ ┆ ┆ 来 ┆