优雅のJava(四)—— 优雅的理解代理模式

专栏导航

优雅のJava(零)—— 面向问题的学习

前言 代理 静态代理 RPC

文如其名 代理模式 就是代理 屏蔽细节 客户用起来感觉是一样的
类似之前的 客户只负责指定标准 接口 输入参数 你怎么返回我不管 我用起来感觉一样就行辣

静态代理的实现本身没啥技术 你直接把要代理的实例对象引用过来 调用它的方法就行啦

比如 本来是远程调用 别人服务器的Hello方法 然后回显数据 客户可不想看到 那些远程连接的一大堆代码 于是我们需要实现客户只是这么调用Stub.hello(); 就能看到回显数据了 那怎么办?Stub就是我们的代理类 这个名字是约定俗成的

这种远程调用也被称为RPC Remote Procedure Call 远程过程(过程:一段程序就叫过程)调用 那其实平常我们调用函数就是“本地过程调用(我就叫他LPC)”咯

客户就想 RPC底层我不想看到 我想使用起来好像本地调用一个函数一样简单 这就是RPC的代理

静态代理 VS 装饰器 VS AOP

其实实现的套路接近 都是拉别人的实例对象进来 加东西(不加也行 就套个壳 那或许变成适配器模式了。。)

只不过装饰器侧重点在装饰 所以是基于原来的做增强 或者说添加特性 这样符合“合成”的思想 而不是继承的思想

但是代理 则是尽量和被代理对象同规范 因为客户指定的规范 客户需要你这样和他对接 所以侧重点是代理

现在代理 往往都是往动态代理 转而往AOP增强的思路前进 那自然必定也有增强 但不是象装饰器那种 装饰一层 添加一个特性 而是只是为了代理 而增强功能而已

虽然两种都能达到一样的效果可能 但是思维不同

动态代理 VS 静态代理

扯了这么多动态代理 那为啥需要动态代理 原来那个不够嘛?拉进来被代理实例 前后做些操作就完事了啊

试想 假设 对面服务器上几百个组件类及其方法调用 你得创造多少代理类啊。。。还有他们的方法 等 毕竟要遵守同一个规范来着 这不折磨

更加折磨的是 如果有个组件的某个方法改了名字 那代码不符合开闭原则 你将会在一堆代码里边修修补补最后迷失自己

其实我们可以发现静态代理很多重复的东西 如果我们能够动态的生成类 和被代理的类同一个规范 那不是完事了吗?这就是所谓动态的代理

RPC调用的例子

我们看个简单的RPC调用的例子
首先 我们的客户端 使用起来还是很快乐 我们远程的服务 规范是UserService接口 然后举例子使用的方法是findUserById

public class Client{
    public static main(String[] args) throws exception {
        IUserService service = Stub.getStub();
        sout(service.findUserById(123));
    }
}

那我们这个stub应该怎么动态代理法?我们先使用jdk原生的动态代理 也就是熟悉的invoke Proxy.newProxyInstance那一套

public class stub{
    public static UserService getStub(){
        // 匿名内部类写好Handler 配置好 要生成的动态类的信息
        InvocationHandler h = new InvocationHandler() {
            @Override 
            public Object invoke(Object proxy, Method method, Object[] args) throws ThrowableException{
                Socket s = new Socket("127.0.0.1", 8888);
                ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
                oos.writeUTF(method.getName());
                oos.writeObject(method.getParameterTypes());
                oos.writeObject(args);
                oos.flush();
            }
        };
        Object o = Proxy.newProxyInstance(UserService.class.getClassLoader(), ... )
        return (UserService) o;
    }
}

这里 invoke里边就是远程通信的具体细节 然后再使用newProxyInstance的方法获取代理生成的对象然后返回給客户使用
这里 这个本地的 getStub返回的对象 代理了远程服务器那个提供服务的对象(你可以这么理解)

当然了 服务器那边其实就需要解析了 解析出来反射一个对象 执行 然后返回结果即可

public class Skeleton{
    private static void process() throws Exception{
        Socket s = new Socket("127.0.0.1", 8888);
        InputStream in = s.getInputStream();
        OutputStream out = s.getOutputStram();
        ObjectInputStream oos = new ObjectInputStream(in);
        DataOutputStream dos = new DataOutputStream(out);
        
        // 开始解包 解出来name 接口参数等等
        String methodName = oos.readUTF();
        Class[] parameterTypes = (Class[])oos.readObject();
        Object[] args = (Object[])oos.readObject();
        
        UserService service = new UserServiceImpl();
        // 反射 获取方法
        Method method = service.getClass().getMethod(methodName, parameterTypes);
        // 调用 获取结果
        User user = (User)method.invoke(service, args);
        // ... 利用结果做什么 返回给client 比如说
        dos.writeInt(user.getId());
        dos.writeInt(user.getName());
        dos.flush();
    }
}

Why not Http?

这个问题其实很简单 按理来说 我们实际上把需要的对象 对象的方法 以及执行需要的参数 传过去 然后对面返回一个JSON结果就好了啊 没啥东西 就是普通的Http请求 然后controller回复就完事儿。。。

所以那个时候客户端可以这么调用:

	public class Client{
	    public static main(String[] args) throws exception {
	        IUserService service = Stub.getStub();
	        sout(service.RPC("findUserById", 123)); // 举例 当然参数的封装自然用些复杂数据结构 更具有兼容性
	    }
	}

确实! 而且这个和动态代理无关其实 我们invoke方法也可以这样来 只不过是接口规范改变了

注意 代理的关键是同一个规范 那自然 原来那种就好像调用本地对象 本地方法的思路 更符合代理模式的精髓

动态代理实现AOP

实际上 技术层面而言 动态代理比较关键的是能够实现AOP增强的代理!

之前RPC的那个远程服务的代理没有增强 只是屏蔽了底层细节

	public Object creatProxy() {
		// ...
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                beforeAdvice.before(); // 前置增强
                Object result = method.invoke(targetObject, args);//调用目标对象的目标方法
                afterAdvice.after(); // 后置增强
                
                return result;
            }
        };
        Object proxyObject = Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
        return proxyObject; // 返回增前的代理对象
    }

这里 我们在原有的被代理对象前后加了东西 这才是前置后置增强 用AOP的思想来说就是 织入

而这么做的好处是 比如实现所有方法的前后打印日志 你动态代理所有的对象 給每个组件对象来个AOP增强 不就成了?

JDK动态代理 VS CGLib动态代理 字节码增强

首先 各位不要小看了的动态代理 以为这玩意也就是 类似静态代理一样 前置 引用执行结果 后置 完事

关键一点在于 静态代理只是代理一段过程 首先被代理的对象存在 然后静态代理用个所谓代理对象的方法 调用被代理的对象 完成套壳的操作 但是动态代理 被代理的对象压根就没出来过!

它是通过织入字节码的方式 我们任何类 方法 本质都是字节码 我往里边改改不就完事了吗:)改完 再用对应的classLoader 类加载器 就能实现运行的时候 产生新的类 而不是只能程序一启动那个时候加载类

这种技术统称 字节码增强技术 只是JDK和CGLib的思路有所不同

JDK动态代理专注于同一种规范 所以要求提供被代理类的

  • 接口
  • 类加载器

如果被代理类没有接口 那根本操作不了 如果有 那我可以用反射 套出来这个类里边有啥方法 方法参数 属性等等 然后增强 等同于造一个新的类 一个符合同一规范的类 然后用类加载器加载出来 你可以发现代理类往往叫“$proxy-序号” 序号只是为了造出来的类不重名罢了

CGLib动态代理 使用的是子类继承父类 从而获取到被代理(父类)的信息 再继承 增强其功能的 所以出来的类就是被代理类的子类哈哈

实际中两种方法并列 Spring源码里边 有以下片段
在这里插入图片描述

可以发现使用哪个 有三个条件

  • 第一 配置 config写啥就是啥

  • 第二 通过这个注解(标记) 这个属性配置的
    在这里插入图片描述
    在 createAopProxy方法里边 根据这个条件选择使用哪个

  • 第三个条件就是 hasNoUserSuppliedProxyInterfaces
    即 bean组件如果有接口 使用JDK 字节码增强技术 $proxy
    没接口使用CGLib

所以看情况使用 但是核心不变 代理增强 动态的造类运动

后记 思维 VS 实现

这篇其实技术讲的尽量少了 关键是思维

其实你说技术上 静态代理和装饰器区别有吗 没有啊 关键不是实现的技术 而是思路 为什么要代理 什么是所谓代理 而什么又是“装饰” 装饰可以叠加 可以像前端那种mixin 代理只能是代理原有的

RPC概念

同样的道理 RPC框架的使用上 有人提问 为啥不使用HTTP?问的人可能太专注与技术本身而没有管思维 RPC调用是种思维 不是技术具体实现的方式 他只是接口 就类似代理模式这个概念一样。

RPC当然可以使用HTTP技术来实现 你制定好访问的接口 传递参数的数据格式 有什么不可以的呢?你甚至可以使用TCP UDP实现。。。之所以使用各种各样的 所谓RPC框架 那是因为他们框架包含更好的 序列化技术实现 信息传输的技术实现

RPC框架的实现依赖两步 序列化和网络信息传输

  • 序列化 用java自带的serializable自然是效率底下的 替代方案JSON传输 速度一般 不适用于分布式 高效的数据传输 而Thrift | Hessian | ProtoBuf 这些就符合要求
  • 传输层面 HTTP1.1问题很大 使用HTTP2.0就很舒服

我们著名的gRPC 其实是结合了HTTP2.0 和 Protobuf 所以gRPC的优点也主要是后面两个大哥提供的

所以 思维是关键 怎么实现又是另外一回事 不能混为一谈

专栏
优雅のJava(零)—— 面向问题的学习

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值