目录
- 一、Spring AOP简介
- 1.什么是AOP
- 2.AOP术语
- 二、AspectJ开发
- 1.基于XML的声明式AspectJ
- 1.1 配置切面
- 1.2 配置切入点
- 1.3 配置通知
- 2.基于注解的声明式AspectJ
一、Spring AOP简介
1.什么是AOP
AOP的全称是Aspect-Oriented Programming,即面向切面编程(也称面向方面编程),是面向对象编程(OOP)的一种补充,目前已成为一种比较成熟的编程方式。
在传统的业务处理代码中,通常都会进行事务处理、日志记录等操作。虽然使用OOP可以通过组合或者继承的方式来达到代码的重用,但如果要实现某个功能(如日志记录),相同的代码仍然会分散到各个方法中。这样,如果想要关闭某个功能,或者对其进行修改,就必须修改所有相关方法。这不但增加了开发人员的工作量,而且提高了代码的出错率。
为了解决这一问题,AOP思想随之产生。AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时再将这些提取出来的代码应用到需要执行的地方。这种采用横向抽取机制的方式,采用传统的OOP思想显然是无法办到的,因为OOP只能实现父子关系的纵向重用。虽然AOP是一种新的编程思想,但却不是OOP的替代品,它只是OOP的延伸和补充。
在AOP思想中,通过Aspect(切面)可以分别在不同类的方法中加入事务、日志、权限和异常等功能。
AOP的使用使开发人员在编写业务逻辑时可以专心于核心业务,而不用过多地关注于其他业务逻辑的实现,这不但提高了开发效率,而且增强了代码的可维护性。目前流行的AOP框架有两个,分别为Spring AOP和AspectJ。Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。AspectJ是一个基于Java语言的AOP框架,从Spring 2.0开始,Spring AOP引入了对AspectJ的支持,AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的植入。
2.AOP术语
AOP术语包括Aspect、Joinpoint、Pointcut、Advice、Target Object、Proxy和Weaving,对于这些专业术语的解释,具体如下。
- Aspect(切面):在实际应用中,切面通常是指封装的用于横向插入系统功能(如事务、日志等)的类,该类要被Spring容器识别为切面,需要在配置文件中通过元素指定。
- Joinpoint(连接点):在程序执行过程中的某个阶段点,它实际上是对象的一个操作,例如方法的调用或异常的抛出。在Spring AOP中,连接点就是指方法的调用。
- Pointcut(切入点):是指切面与程序流程的交叉点,即那些需要处理的连接点。通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以add开头的方法中,那么所有满足这一规则的方法都是切入点。
- Advice(通知增强处理):AOP框架在特定的切入点执行增强处理,即在定义好的切入点处所要执行的程序代码。可以将其理解为切面类中的方法,它是切面的具体实现。
- Target Object(目标对象):是指所有被通知的对象,也称为被增强对象。如果AOP框架采用的是动态的AOP实现,那么该对象就是一个被代理对象。
- Proxy(代理):将通知应用到目标对象之后,被动态创建的对象。
- Weaving(织入):将切面代码插入目标对象上,从而生成代理对象的过程。
二、AspectJ开发
AspectJ是一个基于Java语言的AOP框架,它提供了强大的AOP功能。Spring 2.0以后,Spring AOP引入了对AspectJ的支持,并允许直接使用AspectJ进行编程,而Spring自身的AOP API也尽量与AspectJ保持一致。新版本的Spring框架建议使用AspectJ来开发AOP。使用AspectJ实现AOP有两种方式:
- 一种是基于XML的声明式AspectJ;
- 另一种是基于注解的声明式AspectJ。
1.基于XML的声明式AspectJ
基于XML的声明式AspectJ是指通过XML文件来定义切面、切入点及通知,所有的切面、切入点和通知都必须定义在<aop:config>
元素内。Spring配置文件中的<beans>
元素下可以包含多个<aop:config>
元素,一个<aop:config>
元素中又可以包含属性和子元素,其子元素包括<aop:pointcut>
、<aop:advisor>
和<aop:aspect>
。在配置时,这3个子元素必须按照此顺序来定义。在<aop:aspect>
元素下,同样包含属性和多个子元素,通过使用<aop:aspect>
元素及其子元素就可以在XML文件中配置切面、切入点和通知。常用元素的配置代码如下所示。
<!-- 定义切面Bean -->
<bean id="myAspect" class="com.ssm.aspectj.xmL.MyAspect" />
<aop:config>
<!-- 1.配置切面 -->
<aop:aspect id="aspect" ref="myAspect">
<!-- 2.配置切入点 -->
<aop:pointcut id="myPointCut" expression="execution(* com.ssm.aspectj.*.*(..))" />
<!-- 3.配置通知-->
<!-- 前置通知-->
<aop:before method="myBefore" pointcut-ref="myPointCut" />
<!-- 后置通知 -->
<aop:after-returning method="myAfterReturning"
pointcut-ref="myPointCut" returning="returnVal" />
<!-- 环绕通知-->
<aop:around method="myAround" pointcut-ref="myPointCut" />
<!-- 异常通知-->
<aop:after-throwing method="myAfterThrowing"
pointcut-ref="myPointCut" throwing="e" />
<!-- 最终通知-->
<aop:after method="myAfter" pointcut-ref="myPointCut" />
</aop:aspect>
</aop:config>
1.1 配置切面
在Spring的配置文件中,配置切面使用的是<aop:aspect>
元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,所以要在配置文件中先定义一个普通的Spring Bean(如上述代码中定义的myAspect)。定义完成后,通过<aop:aspect>
元素的ref属性即可引用该Bean。配置<aop:aspect>
元素时,通常会指定id和ref两个属性。
1.2 配置切入点
在Spring的配置文件中,切入点是通过<aop:pointcut>
元素来定义的。当<aop:pointcut>
元素作为<aop:config>
元素的子元素定义时,表示该切入点是全局切入点,可以被多个切面所共享;当<aop:pointcut>
元素作为<aop:aspect>
元素的子元素时,表示该切入点只对当前切面有效。在定义<aop:pointcut>
元素时,通常会指定id和expression两个属性。
在上述配置代码片段中,execution(* com.ssm.jdk.*.*(..))
就是定义的切入点表达式,该切入点表达式的意思是匹配com.ssm.jdk包中任意类的任意方法的执行。
其中execution是表达式的主体。
- 第1个 * 表示的是返回类型,使用 * 代表所有类型;com.ssm.jdk表示的是需要拦截的包名,
- 第2个 * 表示的是类名,使用代表所有的类; 第3个 * 表示的是方法名,使用表示所有方法;
- 后面的()表示方法的参数,其中的“…”表示任意参数。需要注意的是,第1个*与包名之间有一个空格。
上面示例中定义的切入点表达式只是开发中常用的配置方式,而Spring AOP中切入点表达式的基本格式如下:
- modifiers-pattern:表示定义的目标方法的访问修饰符,如public、private等。
- ret-type-pattern:表示定义的目标方法的返回值类型,如void、String等。
- declaring-type-pattern:表示定义的目标方法的类路径,如com.ssm.jdk.UserDaoImpl。
- name-pattern:表示具体需要被代理的目标方法,如add()方法。
- param-pattern:表示需要被代理的目标方法包含的参数,本章示例中目标方法参数都为空。
- throws-pattern:表示需要被代理的目标方法抛出的异常类型。
提示:带有问号(?)的部分(如modifiers-pattern、declaring-type-pattern和throws-pattern)表示可选配置项,其他部分属于必须配置项。
想要了解更多切入点表达式的配置信息,读者可以参考Spring官方文档的切入点声明部分(Declaring a pointcut)。
1.3 配置通知
在配置代码中,分别使用<aop:aspect>
的子元素配置了5种常用通知,这些子元素不支持再使用子元素,但在使用时可以指定一些属性。
【示例3-1】了解了如何在XML中配置切面、切入点和通知后,接下来通过一个案例来演示如何在Spring中使用基于XML的声明式AspectJ,具体实现步骤如下。
(1)创建一个名为chapter03的动态Web项目,导入Spring构架所需求的JAR包到项目的lib目录中,并发布到类路径下。
同时,导入AspectJ框架相关的JAR包,具体如下。 spring- aspects-4.3.6.RELEASE.jar:Spring为AspectJ提供的实现,Spring的包中已经提供。 aspectjweaver-1.8.10.jar:是AspectJ框架所提供的规范,读者可以通过网址(下载)。
(2)在chapter03项目的src目录下创建一个com.ssm.aspectj包,在该包中创建接口UserDao,并在接口中编写添加和删除的方法。
UserDao.java
package com.ssm.aspectj;
public interface UserDao {
//添加用户方法
public void addUser();
//删除用户方法
public void deleteUser();
}
(3)在com.ssm.aspectj包中创建UserDao接口的实现类UserDaoImpl,该类需要实现接口中的方法。
UserDaoImpl.java
package com.ssm.aspectj;
/**
* 功能描述
*
* @author: 衍生星球
* @date: 2023年04月20日 8:20
*/
public class UserDaoImpl implements UserDao {
public void addUser() {
System.out.println("添加用户");
}
public void deleteUser() {
System.out.println("删除用户");
}
}
(4)在chapter03项目的src目录下创建一个com.ssm.aspectj.xml包,在该包中创建切面类MyAspect,并在类中分别定义不同类型的通知。
MyAspect.java
package com.ssm.aspectj.xml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 切面类,在此类中编写通知
*
* @author: 衍生星球
* @date: 2023年04月20日 8:25
*/
public class MyAspect {
//前置通知
public void myBefore(JoinPoint joinPoint) {
System.out.print("前置通知:模拟执行权限检查...");
System.out.print("目标类是:"+ joinPoint.getTarget());
System.out.println(", 被植入增强处理的目标方法:" +joinPoint.getSignature().getName());
}
//后置通知
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("后置通知:模拟记录日志...");
System.out.println("被植入增强处理的目标方法:" +joinPoint.getSignature().getName());
}
/**
*
* 环绕通知
* ProceedingJoinPoint是 JoinPoint的子接口,表示可执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接受一个参数,类型为ProceedingJoinPoint
* 3.必须是 throws Throwable
*/
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//开始
System.out.print("环绕开始:执行目标方法之前,模拟开启事务...");
//执行当前目标方法
Object obj = proceedingJoinPoint.proceed();
//结束
System.out.print("环绕结束:执行目标方法之后,模拟关闭事务...");
return obj;
}
//异常通知
public void myAfterThrowing(JoinPoint joinPoint,Throwable e) {
System.out.print("异常通知:出错了" + e.getMessage());
}
//最终通知
public void myAfter() {
System.out.print("最终通知:模拟方法结束后释放资源...");
}
}
分别定义了5种不同类型的通知,在通知中使用了JoinPoint接口及其子接口ProceedingJoinPoint作为参数来获得目标对象的类名、目标方法名和目标方法参数等。
注意:环绕通知必须接收一个类型为ProceedingJoinPoint的参数,返回值也必须是Object类型,且必须抛出异常。异常通知中可以传入Throwable类型的参数来输出异常信息。
(5)在com.ssm.aspectj.xml包中创建配置文件applicationContext.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:aop="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 1.目标类 -->
<bean id="userDao" class="com.ssm.aspectj.UserDaoImpl" />
<!-- 2.切面 -->
<bean id="myAspect" class="com.ssm.aspectj.xml.MyAspect" />
<!-- 3.aop编程 -->
<aop:config>
<!-- 1.配置切面 -->
<aop:aspect id="aspect" ref="myAspect">
<!-- 2.配置切入点 -->
<aop:pointcut id="myPointCut" expression="execution(* com.ssm.aspectj.*.*(..))" />
<!-- 3.配置通知-->
<!-- 前置通知-->
<aop:before method="myBefore" pointcut-ref="myPointCut" />
<!-- 后置通知 -->
<aop:after-returning method="myAfterReturning"
pointcut-ref="myPointCut" returning="returnVal" />
<!-- 环绕通知-->
<aop:around method="myAround" pointcut-ref="myPointCut" />
<!-- 异常通知-->
<aop:after-throwing method="myAfterThrowing"
pointcut-ref="myPointCut" throwing="e" />
<!-- 最终通知-->
<aop:after method="myAfter" pointcut-ref="myPointCut" />
</aop:aspect>
</aop:config>
</beans>
注意在AOP的配置信息中,使用<aop:after-returning>
配置的后置通知和使用<aop:after>
配置的最终通知虽然都是在目标方法执行之后执行,但它们是有区别的。后置通知只有在目标方法成功执行后才会被植入,而最终通知不论目标方法如何结束(包括成功执行和异常中止两种情况),它都会被植入。另外,如果程序没有异常,异常通知将不会执行。
(6)在com.ssm.aspectj.xml包下创建测试类TestXmlAspectJ,在类中为了更加清晰地演示几种通知的执行情况,这里只对addUser()方法进行增强测试。
TestXmlAspectJ.java
package com.ssm.aspectj.xml;
import com.ssm.aspectj.Dao.UserDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 功能描述
*
* @author: 衍生星球
* @date: 2023年04月20日 11:19
*/
public class TestXmlAspectJ {
public static void main(String[] args) {
//定义配置文件路径
String xmlPath = "com/ssm/aspectj/xml/applicationContext.xml";
//初始化Spring容器,加载配置文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
//获取实例UserDao
UserDao userDao = (UserDao)applicationContext.getBean("userDao");
//执行添加用户方法
userDao.addUser();
}
}
2.基于注解的声明式AspectJ
基于XML的声明式AspectJ实现AOP编程虽然便捷,但是存在一些缺点,那就是要在Spring文件中配置大量的代码信息。为了解决这个问题,AspectJ框架为AOP的实现提供了一套注解,用以取代Spring配置文件中为实现AOP功能所配置的臃肿代码。关于AspectJ注解的介绍如表所示。