设计模式之代理模式
前言
作者最近在学习Spring AOP时,因为AOP(面向切面编程)是使用了代理模式的思想的,要想理解Spring那就得先把代理模式给理解透彻~因为我设计模式的知识忘得比较干净了,所以写这篇博客作为巩固复习,并且为后面学习Spring打磨好刀刃,欢迎读者朋友指正交流。一 学前先知
场景模拟
相信大家对从小到大的一种场景印象是比较深刻的,也可能是比较烦恼的,那就是班级里面要收作业的时候;其实大多数时候,我们的作业都不是老师来收的,而是各科的科代表来收作业,统一收齐后再交给老师,老师也就省下不少功夫可以打理班级的其它事情,同学们也就不用再一个个自己交给老师,而是找科代表就可以了;
这种情景也像极了如今的很多中介,原来我们一开始就和代理模式打交道了,如果你对这种场景有较为深刻的印象,那么继续往下看,其实代理模式不难理解!
模式介绍
这段是比较枯燥的介绍,但是还是非常建议你尽量去对代理模式的介绍有个初步的理解。
代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
模式专业名词的解释:
- 抽象角色:一般使用接口和抽象类来解决
- 真实角色:被代理的角色
- 代理角色:代理真实角色,在原有的实现操作上会有一些附属操作
- 客户:访问代理对象的人
从场景分析和代理模式的实现图中我们可以知道:代理角色(科代表)就是帮助真实角色(同学们)收作业而产生的,除了同学们的收作业的Request外,还可以做其它的工作,比如如果是张三同学要交作业,科代表可以提醒张三他昨天的作业没有提交等等。可以说,代理模式的出现就是要解决直接访问对象时带来的问题,而它的解决方案就是增加中间层来实现。
静态代理和动态代理
在了解代理模式之后,我们需要对代理模式的两种方式进行进一步的学习,即静态代理和动态代理。
静态代理
在程序编译运行前就已经设置好的接口,真实角色类和代理类,即在程序运行前,代理类的.class文件已经生成的代理模式称之为静态代理。
静态代理的实现是比较容易的,但是由于它是在编译运行前就“写死”的,往往不具有相对应的灵活性,它的实现也可以是比较固定化的:
1.创建真实角色(被代理的角色)需要实现的接口,并编写具体的行为。
2.创建真实角色类并实现接口
3.创建代理角色类并实现接口,可以编写对应的扩展逻辑 (预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等)。
4.客户端访问代理角色以间接访问真实角色
动态代理
在了解动态代理之前,先建议你对Java的反射机制有一个清楚的了解和认识,可以看笔者这篇Java基础之反射机制,希望对你有所帮助。
从上面静态代理的初步了解我们可以知道:其代理类是在程序编译运行之前就设置好的,不能随着程序的运行而进行灵活的变化;这时,动态代理的重要性就体现出来了。
动态代理的代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。
- 动态代理和静态代理角色一样
- 动态代理的代理类是动态生成的,而不是提前编译好的
- 动态代理分为两大类:基于接口的动态代理和基于类的动态代理
- 基于接口(JDK的动态代理)
- 基于类(cglib)
- java字节码实现(JAVAssits)
上面的概念算是比较清晰明了的了,如果看得一头雾水,那也可以先抛下概念直接看后面的实例部分,也可以继续往下看对于动态代理的InvocationHandler接口和Proxy类的文档进行辅助理解,不必在概念上死磕。
InvocationHandler接口
JDK1.8中文文档是这样描述InvocationHandler接口的:
InvocationHandler是由代理实例的调用处理程序实现的接口.
说人话就是我们所编写的动态代理角色类所需要实现的接口,实现了这个接口才能实现动态代理
我们再看一下中文文档的方法说明:
InvocationHandler接口中只有一个invoke方法,如果你对Java反射机制有比较清晰的印象的话,就会清楚Method中也有invoke方法,其作用是激活所得到的方法。后面会对此方法有个问题与解释,这里我们只需要知道它的参数含义及它的作用就可以了。
Proxy类
对于Proxy类,JDK1.8的中文文档是这样描述它的:
Proxy提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。
说人话就是:可以直接通过Proxy类调用它内置的静态方法来创建动态代理类的对象(代理角色).
同样地,我们来对它的中文文档进行浏览学习
它有几个较为重要的方法:
方法 | 解释 |
getInvocationHandler(Object proxy) | 返回指定代理实例的调用处理程序 |
getProxyClass(ClassLoader loader, class<?>... interfaces) | 给出类加载器和接口数组的代理类的 java.lang.Class对象 |
newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h) | 返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序 |
我们需要重点关注的方法是newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h),这个方法是创建一个代理实例的方法,它的三个参数分别代表:
- 第一个是生成代理类的类加载器
- 第二个是代理类实现的接口列表,即真实角色和代理角色所需要共同实现的接口
- 第三个是实现的InvocationHandler接口 h
动态代理的原理
关于动态代理的原理分析,可以去看这篇博客,讲的是真心不错的~
二 实例演示
我们依旧拿学生提交作业的例子来完成对代理模式的实现:学生发现直接提交作业给老师比较麻烦,于是同学们先集中将作业提交给科代表,科代表统一收齐作业后再一起提交给老师;科代表收到张三同学的作业时需要提醒他昨天的作业没有提交,收到李四提交的作业时告诉他老师找他
静态代理实例演示
由于本人在学习Spring时遇到的问题,因此顺便用Spring的注解方式实现静态模式,作为对学习的巩固,实现步骤还是一样的~
- 创建真实角色需要实现的接口,这个接口也是代理角色需要实现的
package com.hang.demo1.pojo;
public interface Person {
public void submitHomework();
}
- 创建真实角色类(学生)
package com.hang.demo1.pojo;
import org.springframework.stereotype.Component;
/**
* 学生交作业一个个交给老师太麻烦了,一般由课代表收齐作业后再一起提交给老师
*
*/
@Component
public class Student implements Person{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void submitHomework() {
System.out.println(name+"提交了作业");
}
}
- 创建代理角色类(科代表)
package com.hang.demo1.pojo;
/***
* 课代表收作业,作为整个模式的“中介”,除了代交作业外还可以有其它附属操作
* 比如张三在提交今天的作业前,课代表可以提醒张三昨天的作业没有提交
* 又比如李四在提交今天的作业后,课代表可以通知李四老师找他
*/
public class StudentProxy implements Person{
private Student student;
public Student getStudent() {
return student;
}
public void setStudent(Person student) {
this.student = (Student) student;
}
@Override
public void submitHomework() {
if(student.getName()=="张三"){
System.out.println("张三同学,你昨天的作业没有提交,今天记得补交");
}
//完成真实角色类的任务
student.submitHomework();
if(student.getName()=="李四"){
System.out.println("李四同学,老师找你");
}
}
}
- 客户端类实现代理配置
package com.hang.demo1;
import com.hang.demo1.config.MyConfig;
import com.hang.demo1.pojo.Person;
import com.hang.demo1.pojo.Student;
import com.hang.demo1.pojo.StudentProxy;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
//先使用注解新建两个被代理对象(同学) 别忘记MyConfig.class 把配置类加载进来
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
//不能使用同一个bean id注册,否则这个对象是相同的
Student stu1 = context.getBean("getStudent", Student.class);
stu1.setName("张三");
Student stu2 = context.getBean("student", Student.class);
stu2.setName("李四");
//再新建一个代理对象(课代表)
StudentProxy proxy=new StudentProxy();
//模拟张三被代理
proxy.setStudent(stu1);
proxy.submitHomework();
System.out.println("-----------------------------------");
//模拟李四被代理
proxy.setStudent(stu2);
proxy.submitHomework();
}
}
运行结果及分析:
代理角色除了完成真实角色的工作外,还实现了其它扩展功能。原因是它们都是实现Person接口,可以通过此接口来实现间接访问真实角色。
动态代理实例演示
动态代理的实现其实也并不复杂,由于程序编译运行前拿不到代理角色,因此我们需要通过Java的反射机制动态获得代理角色,这也就是动态代理一词的来源。解释型的话语我都放在了注释中,结合代码可能理解更为深刻~
1.同样需要一个真实角色的实现接口
package com.hang.demo2;
public interface Person {
public void submitHomework();
}
2.真实角色类(学生)— >到这里其实和静态代理是相似的
package com.hang.demo2;
/****真实角色类:学生需要交作业,课代表作为代理角色帮忙交作业给老师
*
*/
public class Student implements Person{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public void submitHomework() {
System.out.println(name+"提交了作业");
}
}
3.动态生成的代理角色类:
package com.hang.demo2;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/***动态生成代理角色类
*
*/
public class ProxyInvocationHandler implements InvocationHandler {
//生成被代理的接口
Person person;
public void setPerson(Person person) {
this.person = person;
}
//生成得到的代理类
public Object getProxy(){
/***三个参数:
* 第一个是生成代理类的类加载器:this.getClass().getClassLoader()
* 第二个是代理类实现的接口列表: 即真实角色和代理角色所需要共同实现的接口,这里指person
* 第三个是实现的InvocationHandler接口 h,直接可以用this指代
*/
return Proxy.newProxyInstance(this.getClass().getClassLoader(),person.getClass().getInterfaces(),this);
}
//处理代理实例,并返回结果(当代理对象被创建出来时会自动执行)
@Override
public Object invoke(Object o, Method method, Object[] args) throws Throwable {
//实现invoke方法前可以做额外操作
before();
//动态代理的本质,就是用反射实现(person:实现的接口对象,args-->实现的方法)
Object result = method.invoke(person, args);
return result;
}
//可以在调用invoke方法前后加入新的方法实现扩展: 面向切面编程(AOP)
public void before(){
Student student= (Student) person;
if(student.getName().equals("张三")){
System.out.println("张三同学,你昨天作业没有提交,记得补交");
}
}
}
4.客户端测试
package com.hang.demo2;
import java.lang.reflect.InvocationHandler;
public class Client {
public static void main(String[] args) {
//真实对象:学生需要提交作业
Student student=new Student();
student.setName("张三");
//代理对象:课代表帮忙提交作业,这里使用动态生成,也没有编写对应的代理类代码
ProxyInvocationHandler pih = new ProxyInvocationHandler();
//设置需要代理的真实角色对象
pih.setPerson(student);
//返回一个代理类:studentProxy --->课代表
Person studentProxy = (Person) pih.getProxy();
//直接调用学生的提交作业的操作,实际上时InvocationHandler的invoke()方法调用method的invoke()方法执行的
studentProxy.submitHomework();
}
}
运行结果及分析:
本例中重点放在ProxyInvocationHandler类中,在此类中并没有直接生成对应的代理实例,而是通过getProxy方法来获得代理对象,这也就是和静态代理不为相同的地方。需要注意的是必须要实现InvocationHandler中的invoke方法中的method.invoke()才能对被代理对象执行相关的操作~
其实对于上述示例中我一开始有个点很费解,就是明明我没有调用代理类的invoke方法,但是却在程序中执行了,在后来我才注意到:
public static Object newProxyInstance(ClassLoader loader,class<?>[] interfaces,InvocationHandler h)
因为newProxyInstance()方法中的第三个参数h就是在调用生成的动态代理方法时,实际上是调用h中的invoke()方法去执行的
也就是说:在代理对象被创建出来的时候,他就自动实现了InvocationHandler 的invoke方法了,并不需要显示地去调用。
动态代理的通用模板
我们还是将动态代理的配置“写死”了的,若将代理对象转换成Object,那么这个将可以变成各个真实角色都适用的通用模板,代码如下:
package com.hang.demo3;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
/***写一个通用的动态代理生成类
*
*/
public class ProxyInvocationHandler implements InvocationHandler {
//被代理的接口
Object target;
public void setTarget(Object target) {
this.target = target;
}
//生成得到的代理类
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
}
//处理代理实例并返回结果
@Override
public Object invoke(Object o, Method method, Object[] args) throws Throwable {
log(method.getName());
Object result = method.invoke(target,args);
return result;
}
//模拟一个日志
public void log(String methodName){
Date date=new Date();
SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("日志文件:在"+ format.format(date) +"调用了"+methodName+"方法!");
}
}
根据上面的代码就可以对所有需要代理的真实角色设置一个动态的代理对象啦!