获取中...

-

Just a minute...

关于这一部分的知识点 我们先从静态代理说起

静态代理

什么是代理? 代理就是:不操作目标对象而是通过一个中间人(代理)来间接操作
最常见的现实例子就是租房问题:你要租房子 不需要直接找到房东 而是通过租房中介 中介帮你去和房东沟通 那么这里面就有几个角色

  • 房源:一般是一个我们定义的接口
  • 房东:拥有房源 所以是一个实现接口的类
  • 中介:也就是代理类 将房东作为自身的属性 方便后续调用租房的动作 而且其中可以添加一些中介能做而房东做不了的动作 在代码之中的体现就是代理类的独有功能
  • 租客:作为一个启动类 主要动作就是找中介然后租房
    比如说以下的例子
    定义一个核心业务接口 在这里也就是出租房屋这件事情
1
2
3
4
//出租房子
public interface Rent {
void rentHouse(); // 出租房子
}

我们从现实角度出发思考 房东肯定拥有这个核心业务的能力 那么在代码之中的体现就是其实现了这个Rent接口

1
2
3
4
5
6
7
//房东
public class Landlord implements Rent {
@Override
public void rentHouse() {
System.out.println("把房子租给你了");
}
}

中介是代理房东出租房子的 那么假设交易完成 其最终是完成出租房子这个动作的人 那么理应其也实现Rent接口 同时它还和房东有所联系 这一点也要在代码之中有所体现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//中介
public class Agent implements Rent {
private Landlord landlord; // 中介手里有房东的联系方式

public Agent(Landlord landlord) {
this.landlord = landlord;
}//有参构造器

@Override
public void rentHouse() {
// 前置增强:中介在出租前做的额外工作
System.out.println("带你看房介绍房子情况");
System.out.println("帮你谈价格");
System.out.println("准备租赁合同");

// 核心:真正调用房东的出租方法
landlord.rentHouse();

// 后置增强:出租完成后的事情
System.out.println("收取中介费");
System.out.println("把钥匙交给你 合同归档");
}
}

我们可以看到在我们定义的中介类之中 其将房东作为自己的私有属性 并且除了核心业务之外 它还能实现很多额外操作

那么最后需要定义的是我们的租客类

1
2
3
4
5
6
7
8
9
10
11
12
13
//租客
public class Tenant {
public static void main(String[] args) {
// 房东出场 由于前文之中定义中介的时候将房东类landload作为其私有成员变量定义 那么在构造的时候就得传入一个实实在在的房东对象 否则agent对象内部的landload就是null 调用agent.rentHouse() 就会报空指针异常
Landlord landlord = new Landlord();

// 中介出场(中介代理房东)
Agent agent = new Agent(landlord);

//租客调用中介的 rentHouse(),相当于间接租到了房子
agent.rentHouse();
}
}

以上部分就是静态代理的整个逻辑了
那么其实总结以下可以看出相关的特性

  • 代理类和目标类实现同一接口
  • 代理类持有目标对象的引用
  • 代理类在调用目标方法前后可以添加额外动作
  • 客户端调用代理类而不是直接调用目标对象
  • 代理类是在编译期写死的(这也就是为什么说是’静态‘)

其中最核心的一点就在于可以在核心业务的前后添加额外动作 最常见的场景就是日志编写

静态代理的不足

刚刚核心业务只有一个就是出粗房屋的这一动作 假设现在核心业务有多个
比如下列的情况

1
2
3
4
5
6
public class Landlord implements Rent {
@Override public void rentHouse() { System.out.println("房东:出租房子"); }
@Override public void repairHouse() { System.out.println("房东:安排维修"); }
@Override public void collectRent() { System.out.println("房东:收租金"); }
@Override public void terminate() { System.out.println("房东:解约"); }
}

那么如果我们还是使用静态代理 就会非常繁琐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Agent implements Rent {
private Landlord landlord;

public Agent(Landlord landlord) { this.landlord = landlord; }

@Override
public void rentHouse() {
System.out.println("中介:带看房、签合同");
landlord.rentHouse();
System.out.println("中介:收中介费");
}

@Override
public void repairHouse() {
System.out.println("中介:联系维修师傅");
landlord.repairHouse();
System.out.println("中介:确认维修完成");
}

@Override
public void collectRent() {
System.out.println("中介:发催收通知");
landlord.collectRent();
System.out.println("中介:确认收款");
}

@Override
public void terminate() {
System.out.println("中介:审核解约条件");
landlord.terminate();
System.out.println("中介:归档合同");
}
}

不仅仅是代码很长 而且维护困难:如果接口新增一个方法 那么代理类必须跟着一起修改 其无法适应接口变化 也无法在运行时灵活生成

那么接下来就轮到今天的主角 动态代理出场了

动态代理

假设刚刚租房的场景之中 一条街有一百个房东 每个房东都要实现租房 维修 收租这些业务 那么按照静态代理的方式 我们就要写一百个中介来实现 但是这很显然是不合适的
动态代理解决这个问题的方式就是:不再手动编写中介这个给类 而是让java在程序运行的时候 根据需求现场打印一个万能的中介机器人
如果说我们想像静态代理一样将对象之间进行绑定 那么在面对多对象的工作的时候 就会导致代码良非常复杂且不够灵活
那么这时候我们就可以使用动态的方式生成对象 之后再进行代理
这其中该涉及两个重要的方法
那么这其中就要涉及三个重要的组成元素

  • 调控这个代理系统的接口
  • 接收代理对象的方法
  • 生成代理对象的方法
    我们注意来分析一下如何实现这三个重要组成部分

InvocationHandler——调控这个代理系统的接口

java.lang.reflect.InvocationHandle
在Java中,InvocationHandler 是一个接口,它是动态代理机制中的核心部分。动态代理允许开发者在运行时创建一个代理类的实例,这个代理类可以用来代替一个真实的对象,从而实现对真实对象的方法调用的控制

这个接口的作用就是处理代理实例上面的方法调用并返回结果 当一个方法再代理实例上面被调用的时候 方法调用会被编码分批额到这个接口的ivoke方法下

本质上其就是附加在代理实例上的一个对象 代理实例上的每一次方法调用 都会被代理实例直接转发给这个 handler 的 invoke 方法

invoke——接收代理对象的方法

1
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

这是InvocationHandle接口的唯一方法

其中的三个参数分别代表

  • Object proxy —— 代理实例本身(即动态生成的那个对象)注意一点:在invoke方法体之中 尽量避免直接调用proxy的任何方法 否则会由于再次进入invoke的原因导致触发递归调用 最终栈溢出

  • Method method —— 被调用的接口方法(java.lang.reflect.Method 对象)代表本次调用的哪个接口方法

  • Object[] args —— 调用该方法时传入的实际参数数组
    在invoke方法之中 我们需要通过method.invoke(目标对象,args)来真正调用真实业务对象的方法 method知识方法的描述 必须绑定一个具体的实例(也就是被代理的目标对象)才能执行

这个方法调用是在后续生成的代理类再每次代理对象的方法被调用之时自动调用的

注意 invoke必须返回一个object 这个返回值会被代理类原封不动的返回给调用者

newProxyInstance——生成代理对象的方法

Proxy.newProxyInstance这个方法负责再运行的时候动态生成代理类并且创建实例

1
2
3
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

其中有三个参数 我们来逐一解析

  • loader——类加载器
    用于加载动态生成的类 几乎总是写成 接口类.class.getClassLoader()
    类加载器用于将JVM动态生成的代理类加载进内存 只要使用和目标接口相同的类即可

  • interface——要实现的接口数组
    传入Class<?>[](接口的 Class 对象数组) 传入生成的代理对象需要具备的方法
    可以传入多个接口new Class[] { Hello.class, Runnable.class } 那么此时生成的代理实例就会同时是 Hello 和 Runnable 类型 值得注意的是 此处传入的必须是接口对象 不能是普通类

  • h——调用处理器
    这一步就是将这我们前文提到的InvocationHandle绑定到即将生成的代理实例之上 之后代理实例之上的每一个方法调用 就会被转发到h.invoke方法几种给处理

实例分析

在这一场景的动态代理之中 接口和目标类我们直接复用静态代理里面的素材即可 无需改动 在这里我们稍微多添加一点业务

1
2
3
4
5
6
//接口
public interface Rent {
void rentHouse(); // 出租房子
void repairHouse(); // 维修房子
// 假设后续还可以增加:collectRent()、terminate() 等
}

定义房东

1
2
3
4
5
6
7
8
9
10
11
//目标对象 房东
public class Landlord implements Rent {
@Override
public void rentHouse() {
System.out.println("[房东] 我把房子租给你了,这是钥匙。");
}
@Override
public void repairHouse() {
System.out.println("[房东] 安排工人来修水管。");
}
}

接下来我们要利用InvocationHandler实现万能中介的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class AgentInvocationHandler implements InvocationHandler {
// 持有真实房东的引用(但这里用 Object,表示它可以代理任何类型的对象)
private Object target;

// 构造方法:在创建手册时,告诉它你要代理谁(哪个房东)
public AgentInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[中介] 带看房、谈价格、准备合同...");

// 这里使用反射,让目标对象(房东)执行当前被调用的方法
Object result = method.invoke(target, args);

System.out.println("[中介] 收中介费、合同归档完成。\n");

return result;
}
}

我们可以清晰的看到 这里面的房东引用并没有写死是谁 而是用了一个object 当使用构造方法的时候 再指定具体是哪一个房东来进行的这次租房委托
不同的房东要实现的业务不同 所以我们也不能直接写死是调用哪个方法 此时使用invoke就可以达成目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.Proxy;

public class Tenant {
public static void main(String[] args) {
// 真实的房东出现
Landlord landlord = new Landlord();

//这一步告诉万能中介是找哪个房东
AgentInvocationHandler handler = new AgentInvocationHandler(landlord);

Rent proxyAgent = (Rent) Proxy.newProxyInstance(
landlord.getClass().getClassLoader(), // 类加载器
landlord.getClass().getInterfaces(), // 接口数组(这里就是 Rent.class)
handler // 绑定 InvocationHandler
);

proxyAgent.rentHouse();

proxyAgent.repairHouse();
}
}

我们来看生成万能中介的这一段核心代码

1
2
3
4
5
Rent proxyAgent = (Rent) Proxy.newProxyInstance(
landlord.getClass().getClassLoader(), // ① 类加载器
landlord.getClass().getInterfaces(), // ② 接口数组
handler // ③ 绑定规则
);
  • 类加载器部分:告诉JVM使用和方通同一个的类加载器来加载接下来再内存之中创建的代理类
  • 接口数组部分:告诉JVM这个代理对象需要实现的方法 直接从房东langdlord上getInterfaaces下来即可
  • 绑定规则就是将我们前面定义的中介的规则赋予它

我们来看后面调用方法这一块的内容 比如

1
proxyAgent.rentHouse();

这一步时 JVM检测proxyAgent是一个动态代理 于是将这个请求打包成(proxy实例,rentHouse方法对象,参数null)丢进第三步绑定handler.invoke(…)方法之中
handler接收到请求之后 根据定义的手册反射调用房东的真是业务

这样一来 就完成了这个动态代理的过程

小结

我们从租房出发 完整走完了java代理模式的两种实现方式

  • 静态代理:简单直观 但是再面对多业务场景时代码复杂 维护成本过高
  • 动态代理:再运行时动态生成代理对象 增强逻辑只写一次 可以服用 但是由于反射调用 性能会略顶于静态代理
    我们通过以上内容的学习可以看到整个动态代理的流程大概是这样
1
2
3
4
5
6
7
租客调用 proxyAgent.rentHouse()

JVM 拦截,转发给 handler.invoke()

invoke() 执行前置增强 → method.invoke(房东) → 执行后置增强

返回结果给租客

我们需要知道 动态代理的本质是再运行的时候动态生成字节码并加载 让代理类不再存在于源代码之中 而是由JVM再内存之中现场创建

参考文章

相关文章
评论
分享
  • 社会工程学——cupp获取windows密码

    这其实本质是一个社会工程学问题至于为什么 在文章的最后想必大家也能理解假设现在有一个人短时间离开电脑之后并未锁屏 那么旁人就可以通过U盘拷贝重要密码文件的方式获取他的密码 密码文件获取windows并不会直接存储你的明文密码 它存的是...

    社会工程学——cupp获取windows密码
  • URLDNS链入门

    在一些测试环境之中 目标是否发生反序列化是无回显的 是否有一种办法能够测试目标是否能够发生反序列化呢?有的兄弟有的如果我们能让目标服务器通过反序列化发起一次DNS解析请求 那么在接收到请求的时候不就知道了吗那么这就是我们需要学习的UR...

    URLDNS链入门
  • 无痛入门java反序列化

    在学习URLDNS链以及之后的cc链之前 我们得先掌握java反序列化的基础知识 序列化和反序列化序列化把一个java对象转换成字节序列以便存储或者通过网络传输反序列化把字节序列还原成java对象 12序列化:对象->字符串反...

    无痛入门java反序列化
  • 动态加载字节码

    动态加载字节码是一种让JVM可以接收由程序在运行时动态产生的字节数组并将其装在的能力 本义是让程序变得更加灵活 但是也产生了一定的安全问题 核心方法在java.lang.ClassLoader类中有一个方法 1protected fi...

    动态加载字节码
  • 类加载机制漫谈

    解释java字节码为什么需要编译成class文件我们首先要理解JVMjava是是一种混合型语言有编译的步骤也有解释步骤我们编写的java文件需要被编译成class文件之后才能被JVM运行 什么是class文件传统编译型语言(C C++...

    类加载机制漫谈
  • 反射随笔

    反射概念在java之中 万物都是对象 包括“类本身也是一个对象”(属于java.lang.Class类的对象) 反射的本质就是在程序运行期间 通过操控这个Class对象 反向解析和操控它所代表的哪个类里面的内容(构造器,属性,方法) ...

    反射随笔
  • LangChain反序列化

    最近在打SCTF的时候遇到一个LangChain反序列化的题目 比较新颖 于是想着学习一下 LangChain介绍LangChain是一个用于构建大语言模型应用的开源框架 通过这个可以把常用的组件抽象成可串联的链(Chain)举个例...

    LangChain反序列化
  • filter-chain漏洞演示

    这个漏洞链被称为CNEXT 由安全员Charles Fol披露 核心思想是利用系统内置的看似无害的模块(流过滤器)的巧妙组合 在受限的web环境之中执行命令 原理讲解filter协议对于filter伪协议 我们肯定都不陌生这个伪协议允...

    filter-chain漏洞演示
  • 死亡绕过

    前言以及环境介绍本文是对p神文章谈一谈php://filter的妙用 | 离别歌知识点的学习 本文主要介绍使用filter伪协议进行死亡绕过的三种方式 测试环境:小皮面板测试代码:exit.php 12345<...

    死亡绕过
  • Phar反序列化漏洞演示

    前言我们在之前学习的反序列化漏洞 无一不是利用了unserialize()这个函数 但是我们没有去考虑 假设此时的题目并没有给我们这个函数 我们还能否实现反序列化呢 其实是可以的 在2018年的BlackHat 大会上 Sam Tho...

    Phar反序列化漏洞演示
Please check the parameter of comment in config.yml of hexo-theme-Annie!