代理模式

代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象。
这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。
这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码或者方法,如果需要修改,可以通过代理的方式来扩展该方法。

Java代理的分类

大体上可以分为动态代理和静态代理两类,但在实现上又可以分为三种:
(1)静态代理
(2)基于JDK的动态代理
(3)基于CGLIB的动态代理
下面我们来说说这三种代理。

静态代理

静态代理需要代理对象和被代理对象实现一样的接口。
特点:代理类和被代理类在编译期间,就确定下来了;
缺点:较为冗余,若需要给几个类实现代理,则需要写好几个的代理类,不易于维护和理解。

示例代码:

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
34
35
interface ClothFactory{
void produceCloth();
}

//代理类
class ProxyClothFactory implements ClothFactory{
private ClothFactory factory;

public ProxyClothFactory(ClothFactory factory){
this.factory = factory;
}

@Override
public void produceCloth() {
System.out.println("代理类做一些准备工作");
factory.produceCloth();
System.out.println("代理类做一些收尾工作");
}
}

//被代理类
class NikeClothFactory implements ClothFactory{
@Override
public void produceCloth() {
System.out.println("Nike工厂生产一批衣服");
}
}

public class StaticProxyTest {
public static void main(String[] args) {
NikeClothFactory nikeClothFactory = new NikeClothFactory();
ProxyClothFactory proxyClothFactory = new ProxyClothFactory(nikeClothFactory);
proxyClothFactory.produceCloth();
}
}

输出如下:

1
2
3
代理类做一些准备工作
Nike工厂生产一批衣服
代理类做一些收尾工作

动态代理

动态代理是指程序运行时根据需要,动态的在内存中创建目标类的代理对象,从而实现对目标对象的代理功能。
动态代理相比于静态代理的优点:
抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。
实现动态代理通常有这两种方式:基于JDK的动态代理、基于CGLIB的动态代理。

基于JDK的动态代理

JDK的动态代理利用了JDK API,只能为接口创建代理,主要涉及到了java.lang.reflect包中的两个类:Proxy和InvocationHandler。
其中 InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,在并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。
而Proxy则为InvocationHandler实现类动态创建一个符合某一接口的代理实例。

实现代理时,Proxy类会使用到newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:

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

注意该方法是在Proxy类中是静态方法,且接收的三个参数依次为:

ClassLoader loader :指定当前目标对象使用类加载器,获取加载器的方法是固定的;
Class<?>[] interfaces :目标对象实现的接口的类型,使用泛型方式确认类型;
InvocationHandler h :事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入。

实现代理时,需要用到InvocationHandler接口,该接口定义了一个 invoke(Object proxy, Method method, Object[] args)的方法,其中的参数释义为:
proxy :是代理实例,一般不会用到;
method :是代理实例上的方法,通过它可以发起对目标类的反射调用;
args :是通过代理类传入的方法参数,在反射调用时使用。

基于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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
interface Human{
String getBelief();
void eat(String food);
}

//被代理类
class SuperMan implements Human{
@Override
public String getBelief() {
return "I believe I can fly";
}

@Override
public void eat(String food) {
System.out.println("我喜欢吃" + food);
}
}

class ProxyFactory{
//调用此方法,返回一个代理类的对象,解决问题一
public static Object getProxyInstance(Object obj){
//obj:被代理类的对象
MyInvocationHandler handler = new MyInvocationHandler();
handler.bind(obj);

return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler);
}
}

class MyInvocationHandler implements InvocationHandler{

private Object obj;//需要使用被代理类的对象进行赋值

public void bind(Object obj){
this.obj = obj;
}

//当我们通过代理类的对象,调用方法a时,同时会自动的调用如下的方法:invoke()
//将被代理类要执行的方法a的功能,声明在invoke()中
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始...");
//method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
//obj:被代理类的对象
//通过反射机制调用目标对象的方法
Object returnValue = method.invoke(obj, args);
System.out.println("结束...");
//此返回值就是被代理类对象调用相应方法的返回值
return returnValue;
}
}

public class ProxyTest {
public static void main(String[] args) {
SuperMan superMan = new SuperMan();
//注意,这里的Human不能写成SuperMan;因为动态代理会根据你实现的接口去生成对应的代理对象,不保证一定是SuperMan类型的
Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
//当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
String belief = proxyInstance.getBelief();
System.out.println(belief);
proxyInstance.eat("apple");
}
}

输出如下:

1
2
3
4
5
6
开始...
结束...
I believe I can fly
开始...
我喜欢吃apple
结束...

基于CGLIB的动态代理

使用JDK创建代理有一个限制,即JDK动态代理只能为接口创建代理,这一点我们从Proxy的接口方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)就看得很清楚,第二个入参interfaces就是为代理实例指定的实现接口。

现在的问题是:对于没有通过接口定义业务方法的类,如何动态创建代理实例呢?JDK的代理技术显然已经黔驴技穷,CGLib作为一个替代者,填补了这个空缺。
CGLib实现动态代理时,需要实现MethodInterceptor接口,需要重写intercept方法,这里先来解释一下该方法的参数含义:

@param1 obj :代理对象本身
@param2 method : 被拦截的方法对象
@param3 args:方法调用入参
@param4 proxy:用于调用被拦截方法的方法代理对象

基于CGLIB的动态代理的示例代码如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//被代理类
class SuperMan{
public void getBelief() {
System.out.println("I believe I can fly");
}

public void eat() {
System.out.println("我喜欢吃");
}
}

class ProxyFactory implements MethodInterceptor {
private Object obj;//需要使用被代理类的对象进行赋值

public ProxyFactory(Object obj) {
this.obj = obj;
}

//给目标对象创建一个代理对象
public Object getProxyInstance(){
//Enhancer可以用来为无接口的类创建代理;它会根据某个给定的类创建子类,并且所有非final的方法都带有回调钩子。
//1.工具类
Enhancer en = new Enhancer();
//2.设置父类
en.setSuperclass(obj.getClass());
//3.设置回调对象为本身
en.setCallback(this);
//4.通过字节码技术动态创建子类实例
return en.create();
}

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("开始...");
//写法一:
/*
通过反射机制调用目标对象的方法
Object invoke = method.invoke(obj, objects);
*/
//写法二:(正确)
/*
通过FastClass的机制来实现对被拦截方法的调用
*/
Object returnValue = methodProxy.invokeSuper(o, objects);
System.out.println("结束...");
return returnValue;
}
}

public class CGLIBProxyTest {
public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory(new SuperMan());
SuperMan proxyInstance = (SuperMan) proxyFactory.getProxyInstance();

proxyInstance.getBelief();
proxyInstance.eat();
}
}

输出如下:

1
2
3
4
5
6
开始...
I believe I can fly
结束...
开始...
我喜欢吃
结束...

总结:

intercept方法因为具有MethodProxy proxy参数的原因,不再需要代理类的引用对象了,直接通过proxy对象访问被代理对象的方法(这种方式更快)。
当然也可以通过反射机制,通过method引用实例:
Object result = method.invoke(target, args) 的形式反射调用被代理类方法。

JDK动态代理的拦截对象是通过反射的机制来调用被拦截方法的,反射的效率比较低,所以CGLIB采用了FastClass的机制来实现对被拦截方法的调用。FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。

FastClass使用:动态生成一个继承FastClass的类,并向类中写入委托对象,直接调用委托对象的方法。
FastClass逻辑:在继承FastClass的动态类中,根据方法签名(方法名字+方法参数)得到方法索引,根据方法索引调用目标对象方法。
FastClass优点:FastClass用于代替Java反射,避免了反射造成调用慢的问题。

基于JDK和基于CGLIB的动态代理的区别

JDK的动态代理必须具备四个条件:

  • 目标接口
  • 目标类
  • 拦截器(处理器)
  • 代理类

1、因为利用JDKProxy生成的代理类实现了接口,所以目标类中所有的方法在代理类中都有。
2、生成的代理类的所有的方法都拦截了目标类的所有的方法。而拦截器(处理器)中invoke方法的内容正好就是代理类的各个方法的组成体。
3、利用JDKProxy方式必须有接口的存在。
4、invoke方法中的三个参数可以访问目标类的被调用方法的API、被调用方法的参数、被调用方法的返回类型。

基于CGLIB的动态代理:
1、CGlib是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。
2、用CGlib生成代理类是目标类的子类。
3、用CGlib生成代理类不需要接口。
4、用CGLib生成的代理类重写了父类的各个方法。
5、拦截器中的intercept方法内容正好就是代理类中的方法体。

动态代理总结

JDK动态代理所创建的代理对象,在JDK 1.3下,性能强差人意。虽然在高版本的JDK中,动态代理对象的性能得到了很大的提高,但是有研究表明:
CGLib所创建的动态代理对象的性能依旧比JDK的所创建的代理对象的性能高不少(大概10倍)。
而CGLib在创建代理对象时性能却比JDK动态代理慢很多(大概8倍)。

所以对于singleton的代理对象或者具有实例池的代理,因为不需要频繁创建代理对象,所以比较适合用CGLib动态代理技术,反之适合用JDK动态代理技术。此外,由于CGLib采用生成子类的技术创建代理对象,所以不能对目标类中的final方法进行代理。