代理模式
文章首发于我的个人博客,欢迎访问:https://blog.itzhouq.cn/proxy
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。代理模式的优势是实现了无侵入的代理扩展,也就是方法的增强;让你可以在不用修改源码的情况下,增强一些方法。
为什么要学习代理模式呢?因为 Spring AOP 的底层就是代理模式。
现在模拟一个场景:房东需要出租自己的房子。
实际落地时候将出租这个动作抽象为接口:
/**
* 租房的接口
*/
public interface Rent {
public void rent();
}
房东(Host)实现了这个接口,执行一个租房的方法。
/**
* 房东
*/
public class Host implements Rent {
@Override
public void rent() {
System.out.println("房东要出租房子");
}
}
这个例子在日常生活中很常见。但是我们考虑到实际情况,并不是所有的房东都有那么多自由的时间和资源,不上班就为了把自己的房子租出去的,为了省事他们都会借助于第三方公司做这个事情,就是我们常说的中介,这里的中介就是这个代理对象。
1、静态代理
为什么要加一个代理对象呢?因为代理对象可以做一些额外的操作。比如这里中介除了可以帮房东租房子以外,还能带客户看房子,签合同,收中介费等。
上面的接口和类不需要变动,现在增加代理的功能。
/**
* 代理类:其作用是帮房东租房子
*/
public class Proxy implements Rent {
private Host host;
public Proxy() {
}
public Proxy(Host host) {
this.host = host;
}
@Override
public void rent() {
seeHouse();
host.rent();
fare();
}
public void seeHouse() {
System.out.println("中介带你看房子");
}
public void fare() {
System.out.println("中介收中介费");
}
}
模拟租房的过程:
/**
* 模拟租房的过程
*/
public class Client {
public static void main(String[] args) {
// 房东要租房子
Host host = new Host();
// 代理角色:中介,帮房东租房子,除此之外提供一些额外的服务
Proxy proxy = new Proxy(host);
// 你不用面对房东,直接找中介即可
proxy.rent();
}
}
运行的结果:
中介带你看房子
房东要出租房子
中介收中介费
可以看到代理类对被代理的对象进行了增强。
角色分析:
- 抽象角色:一般会使用接口或者抽象类解决
- 真实角色:被代理的对象
- 代理角色:代理真实角色,代理真实角色后,我们一般会做一些附属操作
- 客户:访问代理对象的人。
2、静态代理举例
再看一个实际开发中遇到的问题。有个接口UserService
,其实现类 UserServiceimpl
中有很多方法。现在有个需求,需要在执行的方法前面添加日志,知道哪个方法执行了,但是不改变原有的UserServiceimpl
实现类。
如果直接修改这个实现类会有以下几个问题:
- 可能这个实现类不能让你直接修改
- 这个实现类可以修改,但是修改的话有增加 BUG 的风险
- 这个实现类可以修改,但是实现类中方法很多,需要添加大量重复的代码
这个时候代理模式就能排上用场了。
接口:
public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}
实现类:
/**
* 真实对象
*/
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("增加了一个用户");
}
@Override
public void delete() {
System.out.println("删除了一个用户");
}
@Override
public void update() {
System.out.println("更新了一个用户");
}
@Override
public void query() {
System.out.println("查询了一个用户");
}
}
/**
* 模拟用户的操作
*/
public class UserAction {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
userService.add();
}
}
现在模拟添加日志的需求:
添加一个代理类,实现 UserService
接口,注入UserServiceImpl
,添加日志的方法:
public class UserServiceProxy implements UserService {
private UserServiceImpl userService;
public UserServiceProxy(UserServiceImpl userService) {
this.userService = userService;
}
@Override
public void add() {
log("add");
userService.add();
}
@Override
public void delete() {
log("delete");
userService.delete();
}
@Override
public void update() {
log("update");
userService.update();
}
@Override
public void query() {
log("query");
userService.query();
}
// 日志方法
public void log (String msg) {
System.out.println("执行了" + msg + "方法");
}
}
模拟用户操作:
/**
* 模拟用户的操作
*/
public class UserAction {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
// userService.add();
UserServiceProxy userServiceProxy = new UserServiceProxy(userService);
userServiceProxy.add();
}
}
运行结果:
执行了add方法
增加了一个用户
通过这个例子,可以很好的理解静态代理模式的使用场景和使用方法。
3、动态代理
- 动态代理和静态代理角色一样
- 动态代理的代理类是动态生成的,不是我们直接写好的。
- 动态代理分为两大类:基于接口的动态代理和基于类的动态代理
- 基于接口 --- JDK 动态代理
- 基于类 --- cglib
- Java 字节码 --- javassist
3、1 JDK 自带的动态代理
- java.lang.reflect.Proxy:生成动态代理类和对象;提供了静态方法,可以创建动态代理类和实例。
- java.lang.reflect.InvocationHandler(处理器接口):可以通过invoke方法实现对真实角色的代理访问。
每次通过 Proxy 生成的代理类对象都要指定对应的处理器对象。
下面的代码演示了,如何通过 JDK 的动态代理生成一个特定代理对象,进而对被代理对象进行增强。
接口:Subject.java
public interface Subject {
public int sellBooks();
public String speak();
}
真实对象:RealSubject.java
public class RealSubject implements Subject {
@Override
public int sellBooks() {
System.out.println("卖书");
return 1;
}
@Override
public String speak() {
System.out.println("说话");
return "张三";
}
}
处理器对象:MyInvocationHandler.java
等下就是使用这个处理器对真实对象的方法进行增强。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* 处理器对象
*/
public class MyInvocationHandler implements InvocationHandler {
/**
* 因为需要处理真实对象,需要把真实角色传进来
*/
Subject realSubject;
public MyInvocationHandler(Subject realSubject) {
this.realSubject = realSubject;
}
/**
* @param proxy 代理类
* @param method 正在调用的方法
* @param args 方法的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用代理类");
if (method.getName().equalsIgnoreCase("sellBooks")) {
int invoke = (int) method.invoke(realSubject, args);
System.out.println("调用的是卖书的方法");
return invoke;
} else {
String string = (String) method.invoke(realSubject, args);
System.out.println("调用的是说话的方法");
return string;
}
}
}
调用端进行测试:Main.java
import java.lang.reflect.Proxy;
/**
* 调用类
*/
public class Main {
public static void main(String[] args) {
// 真实对象
Subject realSubject = new RealSubject();
// 根据 真实对象 创建一个 处理器对象
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(realSubject);
// 代理对象:这个代理对象是通过 Proxy 类的静态方法,动态生成的
Subject proxyClass = (Subject) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Subject.class}, myInvocationHandler);
// proxyClass.sellBooks();
proxyClass.speak();
}
}
执行结果:
调用代理类
说话
调用的是说话的方法
从结果可以看到通过生成代理对象,对真实对象的方法进行了增强。
值得注意的是,这个处理类对象myInvocationHandler
其实是Proxy.newProxyInstance
方法的一个参数,可以写成匿名内部类。这个可以看我之前写的笔记增强一个对象的方法的三种方式,那里就是这么写的。
3、2 Cglib 动态代理
Cglib 动态代理是针对代理的类,动态生产一个子类,然后子类覆盖代理类中的方法。如果是final
或是 private
修饰的,则不会被重写。Cglib 是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为 JDK 的动态代理提供了很好的补充。通常可以使用 Java 的动态代理创建代理,但当要代理的类没有实现接口或为了更好的性能,Cglib 是一个好的选择。
Cglib 作为一个开源项目,其代码托管在在 GitHub ,地址为: https://github.com/cglib/cglib 。
使用 Cglib 需要导入相关依赖或者 jar 包。我这里导入 jar 包,下载地址: https://github.com/cglib/cglib/releases/tag/RELEASE_3_3_0 。除此之外,Cglib正常与运行还需要asm.jar
的支持( cglib 底层使用字节码处理框架ASM,来转换字节码并生成新的类 ),下载地址: https://mvnrepository.com/artifact/org.ow2.asm/asm/7.2 。
注意:如果没有导入asm
的依赖会抛出异常:
Exception in thread "main" java.lang.NoClassDefFoundError: org/objectweb/asm/Type
下面通过一个简单的例子来说明其使用方式:
需要被代理的类:
package cn.itzhouq.proxy.cglib;
/**
* 被代理类
*/
public class Engineer {
// 可以被代理
public void eat() {
System.out.println("工程师正在吃饭");
}
// final 方法不会被生成的子类覆盖
public final void work() {
System.out.println("工程师正在工作");
}
// private 方法不会被生成的子类覆盖
private void play () {
System.out.println("工程师正在玩游戏");
}
}
可以看到这个被代理的类是没有实现接口的。
Cglib 代理类:
package cn.itzhouq.proxy.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* CGLIB 代理类
*/
public class CglibProxy implements MethodInterceptor {
private Object target;
public CglibProxy(Object target) {
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("### before invocation");
Object result = method.invoke(target, objects);
System.out.println("### end invocation");
return result;
}
public static Object getProxy(Object target) {
Enhancer enhancer = new Enhancer();
// 设置需要代理的对象
enhancer.setSuperclass(target.getClass());
// 设置代理人
enhancer.setCallback(new CglibProxy(target));
return enhancer.create();
}
}
测试方法:
package cn.itzhouq.proxy.cglib;
/**
* 测试方法
*/
public class CglibMainTest {
public static void main(String[] args) {
// 生成 Cglib 代理类
Engineer engineerProxy = (Engineer) CglibProxy.getProxy(new Engineer());
// 调用相关方法
engineerProxy.eat();
engineerProxy.work();
// engineerProxy.play(); // 该代理对象中没有该方法
}
}
运行结果:
### before invocation
工程师正在吃饭
### end invocation
工程师正在工作
通过这种代理方式,也可以对方法进行增强。被 final
修饰的方法不会被生成的代理类(也是子类)覆盖,被private
修饰的方法压根儿不会被代理类继承。
关于 Cglib 的原理,可以参考这篇文章: https://www.runoob.com/w3cnote/cglibcode-generation-library-intro.html 。
4、代理模式和装饰器模式的区别
两者都是对类的方法进行扩展,但是装饰器模式强调的是增强自身,在被装饰之后你能够在被增强的类上使用增强后的功能。增强后你还是你,只不过能力更强了而已;而代理模式则强调的要让别人帮你去做一些本身与你业务没有太多关系的职责(记录日志、设置缓存)。代理模式是为了实现对象的控制 ,因为被代理的对象往往难以直接获得或者其内部不想暴露出来。
关于这个细节,可以这篇知乎回答,作者举的例子很形象。
Java中“装饰模式”和“代理模式”有啥区别? - 知乎 https://www.zhihu.com/question/41988550/answer/462204684
5、代理模式的优缺点
优点:
- 可以使真实角色的操作更加纯粹!不用去关注一些公共的业务
- 公共业务交给代理角色!实现了业务的分工
- 公共业务发生扩展的时候,方便集中管理
缺点:
- 一个真实对象就会产生一个代理对象,代理量会翻倍。