Java代理实践:静态代理、JDK代理、CGLIB代理

Spring AOP可以在不侵入原有代码的情况下实现行为的拓展、增强,其内部就是通过动态代理来实现。本文我们将展示两大类、三种具体的代理方法

abstract.png

简介

通过代理访问的方式,一方面可以屏蔽被代理对象的内部细节;另一方面,我们可以通过代理来拓展、增强被代理对象的功能、行为。在Java中,可根据代理类的生成时机划分为两大类:静态代理、动态代理。前者是在程序运行前通过开发者手动编写实现代理类;而后者则是在运行期根据需要动态地生成代理类字节码文件并创建实例。对于动态代理,有两种具体实现方式:JDK代理、CGLIB代理

静态代理

静态代理类需要开发者自行开发实现。其与被代理类(委托类)必须属于同一种类型,即要么两者实现同一个接口,要么派生自同一个类。这里我们提供了一个Sell接口及实现该接口的Apple类

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
/**
* 卖产品
*/
public interface Sell {
/**
* 卖手机
*/
void sellPhone();

/**
* 卖电脑
*/
void sellPc();
}
...
/**
* 被代理对象: 苹果公司
*/
public class Apple implements Sell {
@Override
public void sellPhone() {
System.out.println("Apple: Sell iPhone SE");
}

@Override
public void sellPc() {
System.out.println("Apple: Sell MacBook Pro");
}
}

如果我们期望能够在Apple实例的方法调用前后执行一些额外的逻辑,但是又不想对Apple类的方法代码造成侵入。一种简单可行的途径就是静态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Apple 静态代理类
*/
public class Agent implements Sell {
private Sell apple;

public Agent(Sell apple) {
this.apple = apple;
}

@Override
public void sellPhone() {
System.out.println("Static Agent: Start");
apple.sellPhone();
System.out.println("Static Agent: End\n");
}

@Override
public void sellPc() {
System.out.println("Static Agent: Start");
apple.sellPc();
System.out.println("Static Agent: End\n");
}
}

从静态代理类Agent的代码我们可以看到,其与被代理类(委托类)是属于同一类型,即实现了同一个接口。通过在内部持有委托类实例的方式来实现将外部调用转发至真正的委托对象;与此同时,代理类通过实现接口方法的方式来增强委托类的行为。下面即是静态代理的测试实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 静态代理测试
*/
public class AgentTest {

public static void testAgent() {
// 创建被代理实例
Apple apple = new Apple();
// 创建静态代理实例
Agent agent = new Agent(apple);

// 通过静态代理实例调用方法
agent.sellPhone();
agent.sellPc();
}
}

测试结果红框即为静态代理所增强的行为

figure 1.png

静态代理虽然简单粗暴,但是缺点同样明显。当接口方法发生变动时,代理类、委托类均需同步修改;其次当委托类过多时,如果只通过一个代理类来实现,会使得该代理类中的代码越来越臃肿。而如果遵循单一功能的设计原则,一个代理类只负责代理一个接口,又会导致代理类的数量过多。那能不能让代理类在运行期动态地按需生成,答案是可以的,这就是我们下面即将介绍的动态代理

动态代理:JDK代理

JDK的动态代理方式,其只支持基于接口的动态代理,即委托类必须实现某个接口。一般地,我们只需关心JDK中的两个核心API:InvocationHandler接口、Proxy类。在实际应用中,我们必须实现InvocationHandler接口的invoke方法。一方面用于定义我们所需增强的行为,另一方面利用反射将方法调用转发给真正的被代理对象

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
/**
* JDK代理: 中介类, 其必须实现InvocationHandler接口
*/
public class JDKProxy implements InvocationHandler {
private Object object;

public JDKProxy(Object o) {
this.object = o;
}

/**
* 动态代理实例内部通过调用该方法,实现行为增强
* @param proxy 动态代理类实例
* @param method 方法
* @param args 方法参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK Proxy: Start");
System.out.println("[proxy class name] : " + proxy.getClass().getName());
System.out.println("[method] : " + method.toString());

// 调用被代理对象相应的方法
Object result = method.invoke(object, args);

System.out.println("JDK Proxy: End\n");
return result;
}
}

前面提到动态代理是在运行期动态生成代理类的字节码并加载到JVM中的,那么这个动态代理类的生成及实例化一般可通过Porxy的newProxyInstance方法实现。从该方法的签名我们可以看出其需接受三个参数:loader参数接收一个类加载器实例,用于加载动态代理类的实例;interfaces参数用于指定该动态代理类所需实现的接口数组;而最后一个参数h则用于接收一个InvocationHandler实例,用于增强委托类的行为

1
2
3
4
5
public class Proxy implements java.io.Serializable {
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
throws IllegalArgumentException
}

下面我们通过一个实际的例子,来看看如何使用JDK代理。可选地,我们可通过System.getProperties().put()方法,将JDK动态生成的代理类字节码文件序列化到硬盘,便于我们后续分析查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JDKProxyTest {
public static void testJDKProxy() {
// 创建被代理实例
Apple apple = new Apple();
// 创建中介类实例
JDKProxy jdkProxy = new JDKProxy(apple);

// [可选地]: 将生成的动态代理类 Class 文件保存到指定路径下
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

// 获取动态代理类实例
Sell sell = (Sell)Proxy.newProxyInstance(
jdkProxy.getClass().getClassLoader(), // 指定ClassLoader类加载器实例, 用于加载动态代理类的实例
new Class[] {Sell.class}, // 指定代理类实例实现的接口类型数组
jdkProxy); // 指定代理类实例所关联的InvocationHandler对象


System.out.println("[sell class name] : " + sell.getClass().getName());

sell.sellPhone();
sell.sellPc();

}
}

从测试结果中,我们可以看出,委托类的行为被成功增强

figure 2.jpeg

由于我们把动态生成的代理类字节码保存到了硬盘,所以我们可以直接打开,来看看其到底是如何实现的,可以看到动态代理内部会持有一个InvocationHandler实例,而InvocationHandler中则进一步持有委托类实例

figure 3.jpeg

动态代理:CGLIB代理

JDK代理看似好像已经很好的解决了静态代理的弊端,但是其也是有一定的局限性。正如我们前文所言,其是基于接口的,那如果我们期望动态地代理一个无接口的委托类,JDK代理就无能为力了。所幸的是,第三方库解决了这个问题——CGLIB (Code Generation Library),其可通过继承委托类的方式来动态生成代理类。由于是第三方库,所以我们需要首先添加其依赖

1
2
3
4
5
6
<!-- cglib proxy -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>

这里我们提供一个没有实现接口的委托类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 被代理对象: 微软公司
*/
public class Microsoft {

/**
* 发布操作系统
*/
public void releaseOS() {
System.out.println("Microsoft: Release Windows 10");
}

/**
* 发布Office
*/
public void releaseOffice() {
System.out.println("Microsoft: Release Office 365");
}
}

类似地,在使用CGLIB代理的过程中,我们需要实现MethodInterceptor接口的intercept方法来增强委托类的行为。示例如下

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
/**
* CGLIB 代理对象
*/
public class CglibProxy implements MethodInterceptor {
private Object object;

public CglibProxy(Object o) {
this.object = o;
}

/**
* 被增加的实例通过调用该方法,实现行为增强
* @param o
* @param method 方法
* @param args 方法参数
* @param proxy
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Cglib Proxy: Start");

Object result = method.invoke(object, args);

System.out.println("Cglib Proxy: End\n");
return result;
}
}

现在我们通过代理来访问调用方法,即可实现行为增强。示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CglibProxyTest {

public static void testCglibProxy() {
// 创建被代理实例
Microsoft microsoft = new Microsoft();

// 创建Cglib代理类实例
CglibProxy cglibProxy = new CglibProxy(microsoft);
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass( microsoft.getClass() );
// 回调方法的参数为Cglib代理类对象
enhancer.setCallback(cglibProxy);

// 获取经Cglib代理后被增加的MicroSoft实例
Microsoft microsoftCglibProxy = (Microsoft) enhancer.create();

microsoftCglibProxy.releaseOS();
microsoftCglibProxy.releaseOffice();
}
}

测试结果如下,可以看到其符合我们的预期

figure 4.png

0%