1 Spring 的 AOP 简介
1.1 OOP开发思路
1.2 什么是 AOP
AOP 为 Aspect Oriented Programming的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP 是 OOP 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
1.3 AOP 的作用及其优势
作用:在程序运行期间,在不修改源码的情况下对方法进行功能增强
优势:减少重复代码,提高开发效率,并且便于维护
1.4 AOP 的底层实现
实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。
1.5 AOP 相关术语
在正式讲解 AOP 的操作之前,我们必须理解 AOP 的相关术语,常用的术语如下:
Target(目标对象):代理的目标对象
Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类
Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义
Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知
Aspect(切面):是切入点和通知(引介)的结合
Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
其中,重要的几个概念:
目标对象target:计划被增强的对象
切点pointcut:计划被增强的对象中的方法
通知advice:附加功能
切面类:通知(增强功能)所在的类,称为切面类切面aspect:切点(要增强的方法)+通知(增强的功能) = 切面。
2 基于AOP的开发步骤
1.编写要增强的类,该类称为目标类
2.编写附加功能,附加功能要编写到一个类中。附加功能称为通知,附加功能所在的类被称为切面类
3.将目标类和切面类中的通知配置到一块
3 基于 XML 的 AOP 开发
3.1 快速入门
3.1.1 导入maven库
<dependencies> <!--导入spring的context坐标,context依赖aop--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.5.RELEASE</version> </dependency> <!-- aspectj的织入 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency> <!--Spring集成Junit测试--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.5.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies>
3.1.2 创建目标接口和目标类
在cn.oldlu.aop包中新建
public interface TargetInterface { public void save(); } public class Target implements TargetInterface { @Override //save方法就是切点,即计划被增强的方法 public void save() { System.out.println("save()方法正在执行,保存数据中"); } }
3.1.3 创建切面类以及通知
在cn.oldlu.aop包中新建
package cn.oldlu.aop; public class MyAspect { //附加功能,被称为通知,通知所在的类称为切面类 public void startTransactional(){ System.out.println("开启事物"); } public void submitTransactional(){ System.out.println("提交事物"); } }
3.1.4 将目标类和切面类的对象创建权交给spring
<!-- 要增强的对象--> <bean id="target" class="cn.oldlu.aop.Target"></bean> <!-- 附加功能--> <bean id="myAspect" class="cn.oldlu.aop.MyAspect"></bean>
3.1.5 在 applicationContext.xml 中配置切面
需要先导入aop命名空间
<?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 class="cn.oldlu.aop.Target"></bean> <!--切面类,里面封装了附加功能--> <bean id="myAspect" class="cn.oldlu.aop.MyAspect"></bean> <!--配置AOP--> <aop:config > <aop:aspect ref="myAspect"> <!--配置切面 切面 = 通知(附加功能)+切入点(被增强的方法)--> <aop:before method="startTransactional" pointcut="execution(public void cn.oldlu.aop.Target.save())"></aop:before> <aop:after method="submitTransactional" pointcut="execution(public void cn.oldlu.aop.Target.save())"></aop:after> </aop:aspect> </aop:config> </beans>
上面代码切入点表达式写了两次,可以抽取出公共的
<!--配置AOP--> <aop:config > <aop:aspect ref="myAspect"> <aop:pointcut id="pc" expression="execution(public void cn.oldlu.aop.Target.save())"/> <!--配置切面 切面 = 通知(附加功能)+切入点(被增强的方法)--> <aop:before method="startTransactional" pointcut-ref="pc"></aop:before> <aop:after method="submitTransactional" pointcut-ref="pc" ></aop:after> </aop:aspect> </aop:config>
3.1.7 测试代码
import cn.oldlu.target.TargetInterface; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class T { @Autowired private TargetInterface target; @Test public void 入门案例测试(){ target.save(); } }
3.1.8 测试结果
开启事物 save()方法正在执行,保存数据中 提交事物
3.1.9 可能出现的异常
Unsatisfied dependency expressed through field 'target'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'cn.oldlu.aop.Target#0' is expected to be of type 'cn.oldlu.aop.Target' but was actually of type 'com.sun.proxy.$Proxy14'
原因:默认spring使用JDK代理,基于接口, @Autowired private TargetInterface target; 这里要用接口接受代理对象
3.2 XML 配置 AOP 详解
3.2.1 切点表达式的写法
表达式语法:
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
- 访问修饰符可以省略
- 返回值类型、包名、类名、方法名可以使用星号* 代表任意
- 包名与类名之间一个点 . 代表当前包下的类,两个点 … 表示当前包及其子包下的类
- 参数列表可以使用两个点 … 表示任意个数,任意类型的参数列表
例如:
execution(public void com.itheima.aop.Target.method()) execution(void com.itheima.aop.Target.*(..)) execution(* com.itheima.aop.*.*(..)) execution(* com.itheima.aop..*.*(..))//第一个* 表示返回值类型,最常用 execution(* *..*.*(..))
3.2.2 切入点的三种配置方式
<aop:config> <!--方式1:配置公共切入点--> <aop:pointcut id="pt1" expression="execution(* *(..))"/> <aop:aspect ref="myAdvice"> <!--方式2:配置局部切入点--> <aop:pointcut id="pt2" expression="execution(* *(..))"/> <!--引用公共切入点--> <aop:before method="logAdvice" pointcut-ref="pt1"/> <!--引用局部切入点--> <aop:before method="logAdvice" pointcut-ref="pt2"/> <!--方式3:直接配置切入点--> <aop:before method="logAdvice" pointcut="execution(* *(..))"/> </aop:aspect> </aop:config>
3.2.3 通知的类型
通知的配置语法:
<aop:通知类型 method=“切面类中方法名” pointcut=“切点表达式"></aop:通知类型>
环绕通知相当于将要写到配置文件里的通知(前置、后置、异常、最终)整合到一个方法里了
如果在XML中不配置其他四种通知,那么可以使用java代码来配置其他四种通知。
环绕通知
public Object around(ProceedingJoinPoint pjp) throws Throwable { //TODO 附加功能 Object ret = pjp.proceed(pjp.getArgs); //调用被增强的方法 //TODO 附加功能 return ret; }
3.2.4 在通知中获取目标方法的实参
格式
public void before(JoinPoint jp) throws Throwable { Object[] args = jp.getArgs(); }
测试步骤
1.创建实体类
package cn.oldlu.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private String name; private Integer age; }
2.创建目标类
package cn.oldlu.aop; import cn.oldlu.domain.User; public interface TargetInterface { public void save(User user); }
package cn.oldlu.aop; import cn.oldlu.domain.User; public class Target implements TargetInterface { //save方法就是切点,即计划被增强的方法 @Override public void save(User user) { System.out.println("save()方法正在执行,保存数据中,姓名:"+user.getName()+"年龄"+user.getAge()); } }
3.创建切面类
package cn.oldlu.aop; import org.aspectj.lang.JoinPoint; import java.util.Arrays; public class MyAspect { //附加功能,被称为通知,通知所在的类称为切面类 public void log(JoinPoint jp){//获取目标方法save中的参数 Object[] args = jp.getArgs(); System.out.println("目标方法的实参是:"+ Arrays.toString(args)); } }
4.修改切入点
因为save方法多了参数所以参数要写…
<aop:pointcut id="pc" expression="execution(public void cn.oldlu.aop.Target.save(..))"/>
5.测试
import cn.oldlu.aop.TargetInterface; import cn.oldlu.domain.User; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class T { @Autowired private TargetInterface target; @Test public void 入门案例测试(){ target.save(new User("tom",3)); } }
6.输出结果
save()方法正在执行,保存数据中,姓名:tom年龄3 目标方法的实参是:[User(name=tom, age=3)]
3.2.5 在通知中获取目标方法的返回值
格式,只能用在afterReturning通知中
public void afterReturning(Object result) { System.out.println(result); }
代码
1.修改TargetInterafce中的save方法添加返回值类型为int
package cn.oldlu.aop; import cn.oldlu.domain.User; public interface TargetInterface { public int save(User user); }
package cn.oldlu.aop; import cn.oldlu.domain.User; public class Target implements TargetInterface { //save方法就是切点,即计划被增强的方法 @Override public int save(User user) { System.out.println("save()方法正在执行,保存数据中,姓名:"+user.getName()+"年龄"+user.getAge()); return 1; } }
2.修改配置文件
配置文件中的切点表达式返回值现在为void,改成int,并且修改通知类型为after-returning
<!--配置切入点--> <aop:pointcut id="pc" expression="execution(public int cn.oldlu.aop.Target.save(..))"/> <!--<aop:before method="startTrasaction" pointcut-ref="pc"></aop:before>--> <aop:after-returning method="log" pointcut-ref="pc" returning="result"></aop:after-returning>
3.修改切面类,获取返回值
package cn.oldlu.aop; import org.aspectj.lang.JoinPoint; import java.util.Arrays; public class MyAspect { public void log(JoinPoint jp,Object result){ Object[] args = jp.getArgs(); System.out.println("目标方法的实参是:"+ Arrays.toString(args)); System.out.println("目标方法的返回值是:"+ result); } }
3.2.6 在通知中获取目标方法抛出的异常信息
方式1,在通知类中使用Throwable中接收异常信息
aop配置
<aop:aspect ref="myAdvice"> <aop:pointcut id="pt4" expression="execution(* *(..)) "/> <aop:after-throwing method="afterThrowing" pointcut-ref="pt4" throwing="t"/> </aop:aspect>
通知类
public void afterThrowing(Throwable t){ System.out.println(t.getMessage()); }
方式2,使用环绕通知,直接使用trycatch捕获
public Object around(ProceedingJoinPoint pjp) throws Throwable { Object ret = pjp.proceed(); //对此处调用进行try……catch……捕获异常,或抛出异常 return ret; }
<aop:aspect ref="myAdvice"> <aop:pointcut id="pt4" expression="execution(* *(..)) "/> <aop:around method="around" pointcut-ref="pt4" /> </aop:aspect>