Spring AOP编程
前言
Spring框架最大的特点封装了大量的设计模式
前面的工厂部分 封装了 工厂模式
而我们要学习的AOP编程 它封装了代理设计模式
1 静态代理设计模式
1.1 为什么需要代理设计模式
1.1.1 问题
-
JavaEE 分层开发中,哪个层次对于我们来讲最重要。
DAO---->Service---->Controller JavaEE分层开发中,最为重要的使Service层
1.1.1 Service层都包含了那些代码?
Service层中.核心功能(几十行,上百行代码)+额外功能(附加功能)
1.核心功能
业务运算
DAO调用
2.额外功能
1.不属于业务
2.可有可无
3.代码量很少
事务,日志,性能.....
----------------------------------------
核心功能
业务,运算
DAO的调用
代码量 几十,上百行代码
额外功能
事务
日志
性能
不属于核心业务
可有可无
代码量 少
额外功能
1.1.1.2 日志
记录用户重要操作的流水账
例如 :用户于2020-09-08转账100给用户B success
1.1.1.3 性能
记录开始时间,记录结束时间 ,结束-开始 核心业务所用时间
1.1.2 额外功能书写在Service层中好不好?
Service层调用者的角度:(Controller):需要在Service层书写额外功能.
软件设计者:Service层不需要额外功能(代码入侵)
-
现实生活中的解决方法
- 房东贴广告 带客户看房太累了.
-
引入一个代理(中介)类
- 1.额外功能
- 调用目标类(原始类)核心功能
- 当对中介不满意时,换一个新中介即可.
1.2 代理设计模式
1.2.1 概念
通过代理类,为原始类(目标)增加新的额外功能
好处:利于原始类(目标)的维护
1.2.2 名词解释
1. 目标类 原始类
指的是 业务类(核心功能-----> 业务运算 DAO运算)
2. 目标方法,原始方法
目标类(原始类)中的方法 就是目标方法(原始方法)
3. 额外功能(附加功能)
日志 事务 性能
1.2.3 代理开发的核心要素
代理类 = 目标类(原始类) + 额外功能 +原始类(目标类)实现相同的接口
房东----->public interface UserService{
m1
m2
}
public class UserServiceImpl implements UserService{
m1 --->业务运算 DAO调用
m2
}
UserServiceProxy implements UserService{
m1
m2
}
1.2.4 编码
1.2.5 静态代理存在的问题
1. 静态类文件数量过多,不利于项目管理
UserServiceImpl UserServiceProxy
OrderServiceImpl OrderServiceProxy
如果有100个原始类 就得有100个对应的代理类
2. 额外功能维护性差
代理类中 额外功能修改复杂(麻烦)
2 动态代理开发
2.1 Spring动态代理的概念
概念:通过代理类为原始类(目标类)增加额外功能
好处:利于原始类(目标类)的维护
2.2 搭建开发环境
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<!--核心包 包括aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
2.3 Spring动态代理的开发步骤
-
创建原始对象(目标对象)
public class OrderServiceImpl implements OrderService{ @Override public void showOrder() { System.out.println("showOrder"); } }
<bean class="com.leetao.proxy.OrderServiceImpl" id="orderService"/>
-
额外功能
MethodBeforeAdvice接口
额外的功能书写在接口的实现中.运行在原始方法执行之前执行额外功能.
public class Before implements MethodBeforeAdvice { @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println("------method before--------"); } }
<bean class="com.leetao.ynamic.Before" id="before" />
-
定义切入点
切入点:额外功能加入的位置 目的:由程序员根据自己的需要,决定额外功能加入给那个初始方法
<aop:config> <aop:pointcut id="pc" expression="execution(* *(..))"/> </aop:config>
4.组合(2,3整合)
表达的含义:所有的方法 都加入 before的额外功能 <aop:advisor advice-ref="before" pointcut-ref="pc"/>
5.调用
目的:获得Spring工厂创建的动态代理对象,并进行调用 ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml"); 注意: 1.Spring的工厂通过原始对象的id值获取的是代理对象 2.获得代理对象后,可以通过声明接口类型,进行对象的存储 UserService userService = (UserService)ctx.getBean("userService");
2.4 动态代理的细节
-
Spring创建的动态代理类在哪里?
Spring框架在运行时,通过动态字节码技术,在JVM创建的,运行在JVM内部,等程序结束后,会和JVM一起消失 什么叫动态字节码技术:通过第三方动态字节码框架,在jvm中创建对应类的字节码,进而创建对象,当虚拟机结束,动态字节码跟着消失 第三方的动态字节码框架 ASM Javassist cglib 直接在jvm生成字节码(动态字节码) 动态代理类 ---->动态字节码 结论:动态代理不需要定义类文件,都是JVM运行过程中动态创建的.所以不会造成静态代理,类文件数量过多,影响项目于管理的问题
-
动态代理编程简化代理的开发
在额外功能不改变的前提下,创建其他目标类(原始类) 的代理对象时,只需要指定原始(目标)对象即可.
3.动态代理额外功能维护性大大提升
3 Spring动态代理详解
3.1 额外功能详解
1. MethodBeforeAdvice接口分析
作用:需要把运行在原始方法执行之前运行的额外功能,书写在before方法中
Method : 增加额外功能的原始方法
login
register
Object[]: 增加额外功能的原始方法的传入参数
Object:原始对象
2. before方法的三个参数,在实战中如何使用
before 方法的参数 在实战中,会根据需要进行使用,不一定都会用到,也有可能都不用.
4 . MethodInterceptor(方法拦截器)
MethodBeforeAdvice只支持在方法执行之前
MethodInterceptor接口:额外功呢给跟据需要运行在原始方法执行的 前,后 前后
public class Arround implements MethodInterceptor {
/**
* invoke 方法的作用:额外功能书写在invoke
* 额外功能 原始方法之前
* 原始方法之后
* 原始方法之前之后
* 确定:原始方法怎么运行
* 参数:MethodInvocation:原始方法 相当于Method
*
* try{
* //TODO 前置增强
* Object object = invocation.proceed() //执行原始方法
* //TODO 后置增强
* //返回值:原始方法执行后的返回值
* return object;
* }catch(Exception e){
* //TODO 异常增强
* }finally{
* //TODO 最终增强
* }
*
* 额外功能运行在原始方法之前
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//原始方法运行之前
System.out.println("1234S");
//执行原始方法
Object proceed = invocation.proceed();
}
}
额外功能运行在原始方法之后
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//执行原始方法
Object proceed = invocation.proceed();
//原始方法之后
System.out.println("54321");
return proceed;
}
额外功能运行在原始方法之前,之后
//什么样的额外功能 运行在原始方法之前,之后都要添加?
//事务,日志
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//原始方法运行之前
System.out.println("1234S");
//执行原始方法
Object proceed = invocation.proceed();
//原始方法之后
System.out.println("54321");
return proceed;
}
额外功能运行在原始方法抛出异常的时候
public Object invoke(MethodInvocation invocation) {
Object proceed = null;
try {
proceed = invocation.proceed();
} catch (Throwable throwable) {
System.out.println("----------原始方法抛出异常----------");
}finally {
return proceed;
}
}
MethodInterceptor 影响原始方法的返回值
原始方法的返回值,直接作为invoke方法的返回值返回,MethodInterceptor不会影响原始方法的返回值
MethodInterceptor影响原始方法的返回值
Invoke方法的返回值,不要直接返回原始方法的运行结果即可。
5 切入点详解
切入点决定额外功能加入位置(方法)
<aop:pointcut id = "pc" expression = "execution(* *(..))"/>
execution(* *(..)) ----->匹配所有的方法
1. execution 执行表达式
* *(..) 切入点表达式
5.1 方法切入点表达式
* *(..) -->所有方法
* ------> 修饰符 返回值
* ------> 方法名
()------> 参数表
.. -----> 对于参数没有要求(有没有,有几个,什么类型都行)
-
定义login方法作为切入点
* login(..)
-
定义login方法且有两个字符串类型的参数作为切入点
* login(String,String) # 注意:非java.lang包的类 必须要写全限定类名 * register(com.leetao.proxy.User) # 限制第一个参数必须为String,后面的参数随意 # ..可以于具体的参数类型连用 * login(String,..) --->login(String,String),login(String)
-
定义匹配public的所有方法
public * *(..)
-
定义匹配public 返回值为void类型的register方法且形参为User的
public void register(com.leetao.proxy.User)
-
上面所讲解的方法切入点不精准
- 两个包都有同名类和同名同参方法
- 使用上面所讲的方法并不会过滤包和类
* *(..)
如果中间的* 没有.只有一个参数时.表示匹配所有包和类的指定方法名 例: * login(..) -->所有包所有类的指定方法名
.. 有没有.有几个
5.2 精准方法切入点限定
修饰符 返回值 包.类.方法(参数)
* com.leetao.proxy.UserService.login(..)
5.3 类切入点
指定特定类作为切入点(额外功能加入的位置),自然这个类中的所有方法,都会加入对应的额外功能
-
语法1
# 类中的所有方法加入额外功能 * com.leetao.proxy.UserServceImpl.*(..)
-
语法2
-
# 忽略包同名类中的所有方法加入额外功能 # 类只存在一级包 * *.UserServiceImpl.*(..) # 类存在多级包 * *..UserServiceImpl.*(..) # .. 没有包,任意包. 对于包没有要求
5.4 包切入点 (实战价值更高)
指定包作为切入点(额外功能加入的位置),包中的所有类,所有方法都会增加额外功能
- 语法1:
# 切入点包中的所有类,必须在proxy中,不能在proxy包的子包中
execution(* com.leetao.proxy.*.*(..))
语法2:
# 切入点当前包及其子包中的所有类,必须在proxy中
execution(* com.leetao.proxy..*.*(..))
5.5 切入点函数
切入点函数:用于执行切入点表达式
-
execution
最为重要的切入点函数,功能最全. 执行 方法切入点表达式 类切入点表达式 包切入点表达式 弊端:execution执行切入点表达式,书写麻烦 execution(* com.leetao.proxy..*.*(..)) 注意:其他的切入点函数 简化的是execution书写复杂度,功能上完全一致.
-
args
作用:主要用于函数(方法) 参数的匹配 切入点:方法参数必须得是2个字符串类型的参数 execution(* *(String,String)) args(String,String)
-
within
作用:主要用于进行类,包切入点表达式的匹配 切入点:UserServiceImpl这个类 execution(* *..UserServiceImpl.*.(..)) within(*..UserServiceImpl) execution(* com.leetao.proxy..*.*(..)) within(com.leetao.proxy..*)
-
@annotation
作用:为具有特殊注解的方法加入额外功能 <aop:pointcut id = "" expression = "@annotation(com.leetao.Log)"/>
5.6 切入点函数的逻辑运算
指的是 整合多个切入点函数一起配合工作.进而完成更为复杂的需求
-
and 与操作
案例:login 同时 参数 2个字符串 execution(* login(String,String)) execution(* login(..)) and args(String,String) 注意:与操作不能用于同种类型的切入点函数
-
or 或操作
案例: register 方法 和 login方法作为切入点 execution(* login(..)) or execution(* register(..))
总结
- 为什么需要代理
-
实现额外功能的接口.
MethodBeforeAdvice 不常用
MethodInterceptor(MethodInvocation)
- 切入表达式
异常处理
AopInvocationException AOP实例异常
MethodInvocationException 方法实例异常
- Null return value from advice does not match primitive return type for: 方法拦截器请设置返回值