一、静态代理设计模式
1.1 为什么需要代理设计模式
问题: 在 javaEE 的分层中,哪个层次对于我们来讲最重要
DAO ---》Service ---》Controller
其实是Service比较重要,这是因为Service是处理业务的层次
DAO其实是辅助Service开发的,而Controller是面向请求的,最终是给到Service的
1.2 Service层中包含了哪些代码?
Service层中 = 核心功能(几十行或上百行) + 额外功能(附加)
1. 核心功能
业务运算:如用户登录.....
DAO调用: 操作数据库
2. 额外功能
1.不属于业务功能
2.可有可无
3.代码量很小
比如: 事务、日志(谁+时间+什么事+结果)、性能(观察业务调用的时间)
1.3 问题:额外功能书写在Service层中好不好?
从Service层的调用者Controller的角度上来看,事务是十分重要的,事务一定在Service中有
从程序的设计者来看,因为额外功能属于可有可无的功能,在某种意义上属于代码入侵,我他妈
不需要它的时候还要删除它,所以不太希望在service层中有额外功能
1.4 现实问题的映射
备注: 房东(软件设计者):不想做额外功能,老子只想收钱
房客(调用者):需要额外功能,不看广告或看房,老子才不给你钱呢
1.5 现实问题的解决方案
我们通过引入一个代理类,去实现额外功能,然后再调用原始类的核心功能,这样就皆大欢喜了,注意下面的中介的业务方法要和房东的业务方法名要保持一致(这样房客才会信嘛)
二. 代理设计模式
2.1 概念
通过代理类,为原始类(目标类)增加额外的功能
好处:利于原始类的维护,毕竟我无需过多关心原始类,对于额外功能我只需关心中介类即可
并且我可以有多个中介类,这样利于代码的扩展性
2.2 名词解释
1.目标类 原始类
指的是 业务类(核心功能 --》业务运算 DAO调用)
2.目标方法 原始方法
目标类(原始类)中的方法 就是目标方法
3.额外功能
2.3 代理开发的核心要素
代理类 = 目标类(原始类) + 额外功能 + 原始类实现相同的接口
房东 ----》 public interface UserService{
m1;
m2;
}
UserServiceImpl implements UserService{
m1; ---> 核心功能 DAO调用
m2;
}
UserServicePorxy implements UserService{
m1;
m2;
}
2.4 编码---小试牛刀(静态代理开发)
第一步: 创建接口及其实现类
接口:
public interface UserService {
//登录
void login(String name,String pwd);
//是否合法
boolean check(String token);
}
实现类:该类就为通俗所称的service层,用来完成业务和DAO调用的
public class UserServiceImpl implements UserService {
@Override
public void login(String name, String pwd) {
System.out.println("UserServiceImpl.login 业务实现 + DAO调用");
}
@Override
public boolean check(String token) {
System.out.println("UserServiceImpl.check 业务实现 + DAO调用");
return false;
}
}
第二步: 创建代理类并实现同一个接口,该代理类带有一些额外功能,注意在这里创建目标类的对象,因为需要他的目标方法的实现
public class UserServiceProxy implements UserService {
// 创建原始类(目标类)的对象
private UserService userService = new UserServiceImpl();
@Override
public void login(String name, String pwd) {
//代理方法提供额外功能 + 原始类的功能
System.out.println("-------log-------");
userService.login(name, pwd);
}
@Override
public boolean check(String token) {
System.out.println("-------log-------");
return userService.check(token);
}
}
第三步: 测试:我们会发现其如期地实现了额外功能和核心功能
小结:所谓的静态代理就是一个Service类,作为程序员就必须手动给它书写一个对应的代理类
存在的问题:
1》 静态类文件过多,不利于项目管理
在上面的案例中
一个UserServiceImpl 对应一个UserServiceProxy
一个OrderServiceImpl 对应一个OrderServieProxy
并且最狗血的是他们提供的额外功能都是提供日志的功能
那么如果我以后又增加了实现类,那我就就需要又增加其代理类的书写
这明显是有不利于管理的
2》额外功能维护性差
就拿上面的例子,如果我额外的功能:打日志,我觉得我实现的不够好,想要
改动它,但是很多地方的代码都耦合了,我要修改的话,那就要动其他地方了
这明显是很恶心的
三、Spring动态代理
3.1 动态代理的概念
概念: 通过代理类为原始类(目标类)增加额外的功能
好处:和静态代理类一样,利于原始类的维护,同时对比静态代理类有更加解耦合的优化实现
3.2 动态代理开发步骤
3.2.1 搭建开发环境
org.springframework
spring-aop
5.1.14.RELEASE
org.aspectj
aspectjrt
1.8.8
org.aspectj
aspectjweaver
1.8.3
3.2.2 思考一个问题
从正常的逻辑出发,我们把一个东西放在另一个东西上面,我们需要什么?我们闭上眼睛想想,显然一定有一个被放物,一个放在被放物的物体上,这里犹如上面所举例的房东和中介,没有了房东,自然不会有中介。有了这个两个物体后,那么进一步的就是“被放物”究竟是什么,有哪些东西?
所以一套aop的开发流程包括:原始类、切入类及其方法、切入点(即放置哪个原始类上)
1. 创建原始类对象(目标类对象) 并 注入
public class UserServiceImpl implements UserService {
@Override
public void login(String name, String pwd) {
System.out.println("UserServiceImpl.login 业务实现 + DAO调用");
}
@Override
public boolean check(String token) {
System.out.println("UserServiceImpl.check 业务实现 + DAO调用");
return false;
}
}
2. 创建切入点类(增强类)并注入
备注:通过实现MethodBeforeAdvice接口的方式额外功能书写在接口的实现中,运行在原始方法执行之前运行额外的功能
public class before implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("------method before advice log -----"); //相当于静态代理类的打印日志
}
}
3.定义切入点并关联切入点类
什么是切入点?
切入点就是额外功能加入的位置
目的:因为这里是写配置文件的,所以能很好地通过配置的切换来实现对不同的原始类进行切入
备注:其实上面的配置可以写成下面
那么我们来想一下,为什么这里要多做一步来对它进行引用?
其实很简单,假设我有另外一个切入类,它的切入点位置是另一个表达式,那么我就可以
切换过来啊,并且通过ref的应用,这样以后若有多个切入类都是这个位置切入,我就直接
引用即可,无需重复写
4.测试你的代码吧
糟糕,报错了
这里报错的原因是因为我忽略了一些重要细节
1. Spring工厂通过原始对象的id值获得的是代理对象,它和上面的实现类都实现同一个
接口,最多算兄弟关系,所以没有继承关系,无法完成互转呢!
2.获取代理对象后,可以通过声明接口类型,来对这个代理类对象进行存储
下图通过debug的方式发现,通过工厂获取的确实是代理类对象,还是jdk的动态代理呢~
小问题: 如果我希望的是通过CGLIB的方式来创建代理类呢?很简单只需要在配置文件的参数配置即可
四、动态代理细节分析
4.1 Spring创建的动态代理在哪里?
首先我们回顾一下静态代理,静态代理是每创建一个实现类就要有一个对应的代理类与之对应
那么Spring的动态代理类在哪里呢?
其实任何程序跑的都不是.java文件,而是其编译的结果.class文件,简称机器码文件
在Spring框架在运行时,通过动态字节码技术,在JVM创建动态代理类对象,该对象是
运行在JVM内部的,当程序结束了,会和JVM一起消失
4.2 什么叫动态字节码技术
所谓的动态字节码技术就是通过动态字节码框架(如下图),在JVM中创建对应类的字节码,进而创建
对象,注意创建的对象是通过字节码去做的,当虚拟机结束,动态字节码跟着消失
4.3 动态代理比静态代理优于哪里?
动态代理不需要定义类文件,都是JVM运行过程中动态创建的,所以不会造成像静态代理那样
代理类文件过多,影响项目管理的问题
其次,动态代理还有一个比较方便的点就是,只需要关心原始类,在额外功能不变的前提下,
直接通过注入对象,即可完成
最后,请你思考一个问题:如果我觉得这个代理类不好用了,我想换其他实现,应该怎么办呢?
其实我们不需要删除原来的方法,我们只需要定义一个新的代理类即可,因为这样才符合开闭原则
五、MethodBeforeAvice详解
public class before implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("------method before advice log -----"); //相当于静态代理类的打印日志
if ((objects[0]) instanceof User) {
User user = (User) (objects[0]);
System.out.println("user.getPwd() = " + user.getPwd());
System.out.println("user = " + user);
}
}
}
参数解释
Method:表示原始方法或目标方法,比如login方法、register方法
Objects[]:原始方法的参数,如String name,String password/User user
Object: 额外功能所增加给的按个原始对象 UserServiceImpl 、OrderServiceImpl
六、MethodInterceptor详解
从上面我们知道实现MethodBeforeAdvice方法可以实现在原始方法执行之前完成执行实现额外功能,但是假如我想执行时间是之后呢?
其实这个接口的实现有点像环绕增强,执行的时机点在之前后之后都是可以的
6.1 实现步骤
第一步: 定义一个类实现MethodIntercetor接口,并实现其方法(注意看里面的注释)
public class Around implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("----这里写运行在原始方法之前的额外功能-----");
Object ret = methodInvocation.proceed();//该行即为原始方法的运行,返回值为原始方法的返回值,如果方法没有返回值,则为null
System.out.println("----这里写运行在原始方法之后的额外功能-----");
return ret;
}
}
备注:
MethodInvocation 有点类似MethodBeforeAdvice方法的参数Method,里面包含一些对原始方法的属性的封装
Object:为原始方法的返回值
第二步:配置文件注入,并组装
第三步: 测试 (还记得吗?调用工厂的getBean方法即可获取其对应的代理类)
@Test
void test02() {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationAopContext.xml");
UserService userService = ((UserService) ctx.getBean("user"));
//userService.check("token");
User user = new User();
user.setPwd("abcd");
userService.checkPerson(user);
}
第四步:输出结果
----这里写运行在原始方法之前的额外功能-----
运行checkPerson方法.....
----这里写运行在原始方法之后的额外功能-----
6.2 问题阐释
6.2.1 什么样的额外功能 运行在原始方法执行之前,之后都要添加呢?
记住之前所说的额外功能无非:日志、事务、性能
其实事务就是一个很好的例子,在原始方法执行之前transaction.begin(),
然后在原始方法执行之后进行transaction.commit()
还有性能也是可以的,比如在原始方法执行之前记录一个时间戳,然后在原始方法执行之后
记录一个时间戳,两者一减,就能看到调用这个方法用了多长时间
6.2.2 那日志用在什么地方呢?
其实看你怎么用啦,如果是想记录这个原始方法抛出异常后记录一个日志的话,
那就可以在原始方法抛出异常的时候进行捕获并记录日志,如下面的代码
public class Around implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object ret = null;
try {
ret = methodInvocation.proceed();
} catch (Exception ex) {
System.out.println("AOP捕获原始方法的异常,记录日志中.....");
}
return ret;
}
}
6.2.3 MethodInterceptor 影响原始方法的返回值
我们知道invoke方法的返回值就是这个代理方法的返回值,proceed方法的返回值为原始
方法的返回值,那么我们当我们通过AOP实现后,如果想影响原始方法的返回值,可以直接
操作invoke方法的返回值,然后返回一个自己想要的值,但是一般不建议这么用,因为本来
代理功能的实际就是为了实现额外功能和核心功能的解耦的,这是属于锦上添花的东西,不能
因此还改变核心方法的功能
七、切入点详解
我们知道动态代理有四个步骤是重要的:原始方法、额外功能、切入点、组装,那么这个切入点到底是什么呢?
这个切入点可以理解为是一个函数表达式,这个表达式声明的是代理方法应该放在哪里。下面为一个demo
execution(* *(..)) ===> 表示匹配所有的方法
1.execution() 切入点函数
2.* *(..) 切入点表达式
7.1 方法切入点表达式
我们先定义一个方法
public void add(int i ,int j)
* *(..)
* *(..) ---> 所有方法
第一个* ---> 修饰符 返回值
第二个* ---> 方法名
() ---> 参数表
.. ---> 对于参数的数量、类型没有要求
7.2 一些方法的定义demo
1.定义login方法作为切入点
* login(..)
2.定义register作为切入点
* register(..)
7.3 稍微复杂一点的切入点表达式
1.定义一个login方法,且方法由两个字符串类型的参数作为切入点
* login(String,String)
注意:
1.1 对于非java.lang包中的类型,必须写全限定名,如下
* checkPerson(cn.paul.spring.demo.hellospring.entity.User)
=====
1.2 还有一种类型可变数组的那样的写法,如下
* login(String,..)
上面这个表达式意味不关注参数的个数和类型,下面的这三个情况都符合要求
* login(String),login(String,String),login(String,cn.paul.spring.demo.hellospring.entity.User)
7.4 更加精准地限定方法名称的切入点表达式
首先我们思考一下,如何精准地定位到一个方法?
其实我们定位一个方法可以通过 包 ---> 类 ---> 方法名 的形式来找到那个方法,所以下面的图就说明了之前得通配符切入点表达式的一般表征与指向
备注: 第二个 * 代表指向任意的包或类或方法
实战一下
问:如果我希望指向特定类的login方法(可能有多个类都含有login方法),怎么写切入点表达式呢?
============
修饰符 返回值 包.类.方法(参数)
* cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl.login(..))
或
* cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl.login(String,String))
问: 如果我希望特定的类的全部方法都包含呢?
* cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl.*(..))
7.5 类切入点
什么是类切入点?
指定特定类作为切入点(额外功能加入的位置),自然这个类中的所有方法都会被代理,被加上
对应的额外功能
7.5.1 语法
1.类中的所有方法都加入特定的额外功能
* cn.paul.spring.demo.hellospring.service.impl.UserServiceImpl.*(..))
问题: 有没有想过一个场景,在不同的包下都有叫UserServiceImpl的类,并且我希望他们都可以被代理?
如问题所述,我们可以将包的位置设为通配符,但是有些注意点要如下注意
1. Spring不会一个个包下帮你递归地去找是否有符合的类,包的通配符使用只限定了
一级包下的类,表达式如下
* *.UserServiceImpl.*(..) =》只对图1有效,对图2无效
------------
如果我希望对图2有效,那表达式该怎么写呢? 如下
* *..UserServiceImpl.*(..) =》对类存在多级包生效 (慎用!)
我们发现在包的通配符位置后面多了两个点,表示中间含有多层的包,让Spring递归地去找!
7.6 包切入点 (最具实战性)
指定包下的全部类及其方法加入额外功能
7.6.1 语法1
#切入点包中的所有类,必须在impl包中,不能在impl包的子包中
* cn.paul.spring.demo.hellospring.service.impl.*.*(..))
7.6.2 语法2
#切入点当前包及其子包都生效
* cn.paul.spring.demo.hellospring.service.impl..*.*(..))
7.7 切入点函数
7.7.1 什么是切入点函数?
你理解函数吗?我的理解的函数就是它就像一个独特的运算流程,我们给它一个定量,它给
我们返回另一个值,而不同的函数可根据其名称做区分
所以切入点函数就是我们给定一个值(切入点表达式)给它,它运算到对应的流程,去到指定的位置去做代理,
从程序员的角度上来看就是用于执行切入点表达式的
7.7.2 有哪些切入点函数?
- execution
这个函数最为重要,能覆盖的情况是最多的
它可以执行:1.方法切入点表达式 2.类切入点表达式 3. 包切入点表达式
弊端: 书写会相对麻烦,不过因为全,麻烦一点也是可以接受的
注意:其他切入点函数,只是简化版的execution表达式,在功能上是完全一致的
- args
这个函数主要用于函数(方法)参数的匹配,意思就是这个执行函数更关注的是【参数】
比如:我希望切入的是方法参数必须是2个字符串类型的参数
1.通过execution函数是这样写的
execution(* *(String,String))
2.通过args函数是这样写的
args(String,String)
- within
主要用于进行类、包切入点表达式的匹配
比如切入点:UserServiceImpl这个类
1.通过execution函数是这样写的
execution(* *..UserServiceImpl.*(..))
1.1 通过within函数是这样写的
within(*..UserServiceImpl)
-------------------------------------------
比如切入点为impl这个包下的所有类的所有方法
2.1 通过execution函数是这样写的
execution(* cn.paul.spring.demo.hellospring.service.impl..*.*(..))
2.2 通过within函数是这样写的
within(cn.paul.spring.demo.hellospring.service.impl..*)
- annotation
作用:为具有特殊注解的方法加入额外功能,即哪个方法贴了这个注解就对该方法加入额外功能
1. 创建一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
2.通过配置文件进行声明
- 切入点函数的逻辑运算
指的是 整合多个切入点函数一起配合工作,进而完成更为复杂的需求
5.1 and 与操作(代表一种需要同时成立的运算)
案例: 对参数为2 个字符串的login方法进行切入
1.使用execution函数表达式
execution(* *login(String,String))
2.使用and与操作
思路: execution函数擅长的是从方法的角度去表达,而args是擅长从方法的参数的角度,
所以可以将两者结合起来
execution(* login(..)) and args(String,String)
注意: and 无法操作同种类型的切入点函数,如下
execution(* login(..)) and execution(* register(..))
为什么? 因为and代表着一种"同时成立",试问有哪个方法既叫login方法,同时也叫register方法呢?
没有吧....
那么如何实现就只针对login方法和register方法的切入呢?=》使用or逻辑符号
execution(* login(..)) or execution(* register(..))
5.2 or 或操作(代表一种需要其一成立即可的运算)具体见and操作符的"注意"