3.1 Spring AOP
3.1.1 什么是AOP
面向切面编程(面向方面编程),是面向对象编程的一种补充。
AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在程序编译或运行时再将这些提取出来的代码应用到需要执行的地方。这种采用横向抽取机制的方式,采用传统的OOP思想显然是无法办到的,以内OOP只能实现父子关系的纵向重用。虽然AOP是一种新的编程思想,但却不是OOP的替代品,它只是OOP的延伸和补充。
在AOP思想中,通过Aspect可以分别在不同类的方法中加入事务、日志、权限和异常等功能。
3.1.2 AOP术语
- Aspect(切面):在实际应用中,切面通常是指封装的用于横向插入系统功能(如事务、日志等)的类,该类要被Spring容器识别为切面,需要在配置文件中通过< bean>元素指定。
- Joinpoint(连接点):在程序执行过程中的某个阶段点,它实际上是对象的一个操作,例如方法的调用或异常的抛出。在Spring AOP中,连接点就是指方法的调用。
- Pointcut(切入点):是指切面与程序流程的交叉点,即那些需要处理的连接点。通常在程序中,切入点指的是类或者方法名,如某个通知要应用到所有以add开头的方法中,那么所有满足这一规则的方法都是切入点。
- Advice(通知增强处理):AOP框架在特定的切入点执行增强处理,即在定义好的切入点处所要执行的程序代码。可以将其理解为切面类中的方法,它是切面的具体实现。
- Target Object(目标对象):是指所有被通知的对象,也成为被增强对象。如果AOP框架采用的是动态的AOP实现,那么该对象就是一个被代理对象。
- Proxy(代理):将通知应用到目标对象之后,被动态创建的对象。
- Weaving(织入):将切面代码插入目标对象上,从而生成代理对象的过程。
3.2 AspectJ开发
使用AspectJ实现AOP有两种方式:一种是基于XML的声明式AspetJ;另一种是基于注解的声明式AspectJ。
3.2.1 基于XML的声明式AspectJ
基于XML的声明式AspectJ是指通过XML文件来定义切面、切入点及通知,所有的切面、切入点和通知都必须定义在< aop:config>元素内。Spring配置文件中的< beans>元素下可以包含多个< aop:config>元素,一个< aop:config>元素中又可以包含属性和子元素,其子元素包括< aop:pointcut>、< aop:advisor>和< aop:aspect>。在配置时,这3个子元素必须按照此顺序来定义。在< 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 expression="execution(* com.ssm.aspectj.*.*(..))" id="myPointCut"/>
<!-- 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>
- 配置切面
在Spring的配置文件中,配置切面使用的是<aop:aspect>
元素,该元素会将定义好的Spring Bean转换成切面Bean,所以要在配置文件中先定义一个普通的Spring Bean,定义完成后,通过<aop:aspect>
元素的ref属性即可引用该Bean。
配置<aop:aspect>
元素时,通常会指定id和ref两个属性。
属性名称 | 描述 |
---|---|
id | 用于定义该切面的唯一标识名称 |
ref | 用于引用普通的Spring Bean |
- 配置切入点
切入点通过<aop:pointcut>
来定义。
当<aop:pointcut>
作为<aop:config>
的子元素来定义时,表示该切入点是全局切入点,可以被多个切面所共享。
当<aop:pointcut>
作为<aop:aspect>
的子元素时,表示该切入点只对当前切面有效。
在定义<aop:pointcut>
时,通常会指定id和expression两个属性。
属性名称 | 描述 |
---|---|
id | 用于定义切入点唯一标识名称 |
expression | 用于指定切入点关联的切入点表达式 |
execution(* com.ssm.aspectj.*.*(..))
就是切入点表达式,意思是匹配com.ssm.aspectj包中任意类的任意方法的执行。
其中execution是表达式主体,第一个 *是返回类型,用 *代表所有类型;第二个 *表示类名,使用 *代表所有的类;第三个 *是方法名,表示所有方法;后面的()表示方法的参数,其中的“…”表示任意参数。
在SpringAOP中切入点表达式的基本格式如下:
execuion(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?
modifiers-pattern
表示定义目标方法的访问修饰符,如public、private等。ret-type-pattern
表示定义目标方法的返回值类型。declaring-type-pattern
表示定义目标方法的路径。name-pattern
表示具体需要被代理的目标方法。param-pattern
表示需要被代理的目标方法包含的参数。throws-pattern
表示需要被代理的目标方法抛出的异常类型。
带有问号的部分表示可选配置项,其他部分属于必须配置项。
- 配置通知
示例3-1
-
创建工程,导入jar包。
spring-aspects-5.2.2.RELEASE.jar
aspectjweaver-1.8.10.jar -
创建com.ssm.aspectj包,创建接口UserDao,编写添加删除方法。
package com.ssm.aspectj;
public interface UserDao {
public void addUser();
public void deleteUser();
}
- 创建接口实现类
package com.ssm.aspectj;
public class UserDaoImpl implements UserDao {
public void addUser() {
System.out.println("添加用户");
}
public void deleteUser() {
System.out.println("删除用户");
}
}
- 创建com.ssm.aspectj.xml包,创建切面类MyAspect,定义不同类型通知
package com.ssm.aspectj.xml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 切面类,在此类编写通知
*/
public class MyAspect {
// 前置通知
public void myBefore(JoinPoint joinPoint){
System.out.println("前置通知:模拟执行权限检查……");
System.out.println("目标类是:"+joinPoint.getTarget());
System.out.println(",被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
// 后置通知
public void myAfterReturning(JoinPoint joinPoint){
System.out.println("后置通知:模拟记录日志……");
System.out.println("被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
/**
* 环绕通知
* ProceedingJoinPoint是JoinPoint的子接口,表示可执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJoinPoint
* 3.必须是throws Throwable
*/
protected Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 开始
System.out.println("环绕开始:执行目标方法之前,模拟开启事务……");
// 执行当前目标方法
Object obj=proceedingJoinPoint.proceed();
// 结束
System.out.println("环绕结束:执行目标方法之后,模拟关闭事务");
return obj;
}
// 异常通知
public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
System.out.println("异常通知:出错了"+e.getMessage());
}
// 最终通知
public void myAfter(){
System.out.println("最终通知:模拟方法结束后释放资源……");
}
}
定义了5种不同类型的通知,使用了JoinPoint接口及其子接口ProceedingJoinPoint作为参数来获得目标对象的类名,目标方法名和目标方法参数等。
- 创建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/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
">
<!-- 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"/>
<!-- 环绕通知-->
<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>
- 创建测试类,对addUser进行增强测试
package com.ssm.aspectj.xml;
import com.ssm.aspectj.UserDao;
import com.sun.javafx.tools.ant.Application;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TsetXmlAspectJ {
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();
}
}
运行结果
要查看异常通知执行效果,可以在addUser中添加出错代码,如int i=10/0
3.2.2 基于注解的声明式AspectJ
基于XML的声明式AspectJ实现AOP编程虽然便捷,但是存在一些缺点,需要配置大量代码信息。
而基于注解的声明式AspectJ解决了这个问题。
示例3-2
- 创建com.ssm.aspectj.annotation包,将MyAspect复制到包下并修改
package com.ssm.aspectj.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 切面类,在此类编写通知
*/
@Aspect
@Component
public class MyAspect {
//定义切入点表达式
@Pointcut("execution(* com.ssm.aspectj.*.*(..))")
//使用一个返回值为void、方法体为空的方法来命名切入点
public void myPointCut(){}
// 前置通知
@Before("myPointCut()")
public void myBefore(JoinPoint joinPoint){
System.out.println("前置通知:模拟执行权限检查……");
System.out.println("目标类是:"+joinPoint.getTarget());
System.out.println(",被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
// 后置通知
@AfterReturning(value = "myPointCut()")
public void myAfterReturning(JoinPoint joinPoint){
System.out.println("后置通知:模拟记录日志……");
System.out.println("被植入增强处理的目标方法为:"+joinPoint.getSignature().getName());
}
/**
* 环绕通知
* ProceedingJoinPoint是JoinPoint的子接口,表示可执行目标方法
* 1.必须是Object类型的返回值
* 2.必须接收一个参数,类型为ProceedingJoinPoint
* 3.必须是throws Throwable
*/
@Around("myPointCut()")
protected Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 开始
System.out.println("环绕开始:执行目标方法之前,模拟开启事务……");
// 执行当前目标方法
Object obj=proceedingJoinPoint.proceed();
// 结束
System.out.println("环绕结束:执行目标方法之后,模拟关闭事务");
return obj;
}
// 异常通知
@AfterThrowing(value = "myPointCut()",throwing = "e")
public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
System.out.println("异常通知:出错了"+e.getMessage());
}
// 最终通知
@After("myPointCut()")
public void myAfter(){
System.out.println("最终通知:模拟方法结束后释放资源……");
}
}
首先使用@Aspect注解定义了切面类,由于该类在Spring中是作为组件使用的,因此还需要添加@Component注解才能生效。
然后使用@Pointcut注解来配置切入表达式,并通过定义方法来表示切入点名称。
接下来在每个通知相应的方法上添加了相应的注解,并将切入点名称“myPointcut”作为参数传递给需要执行增强的通知方法。
- 在目标类UserDaoImpl中添加注解@Repository(“userDao”)
- 创建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/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.ssm"/>
<!-- 启动基于注解的声明式AspectJ支持-->
<aop:aspectj-autoproxy/>
</beans>
首先引入了context约束信息。
然后使用<context>
元素设置了需要扫描的包,使注解生效。
最后使用<aop:aspectj-autoproxy/>
来启动Spring对基于注解的声明式Aspect的支持。
- 在com.ssm.aspectj.annotation包中创建测试类TestAnnotation
package com.ssm.aspectj.annotation;
import com.ssm.aspectj.UserDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestAnnotation {
public static void main (String[] args){
String xmlPath="com/ssm/aspectj/annotation/applicationContext.xml";
ApplicationContext applicationContext=new ClassPathXmlApplicationContext(xmlPath);
UserDao userDao= (UserDao) applicationContext.getBean("userDao");
userDao.addUser();
}
}