Java语言十五讲(第六讲 Proxy代理)

Proxy是代理的意思。在系统搭建的时候,经常会有用到代理的地方,如一个公司访问外网的代理,比如服务器端的反向代理,远程调用代理,还有为了安全或者灵活采取的访问代理,这些都是代理。
日常生活中,我们会经常遇到代理的场景。比如说旅行社,我们一般人没有那么多时间和专业能力安排好自己的行程,就只要告诉代理就可以了,它会把最终的服务提供者如航空公司,铁路,酒店什么的都屏蔽掉,我们只跟代理打交道就行。
计算机系统就是借鉴了现实世界的这个场景。
从程序上来讲,为了访问A对象的方法,有的时候,我们不想直接把A暴露出来,就为A加一个代理,我们访问的是代理,得到的是A的服务。这样做我们隐藏了真正的服务类,可以升级替换,同时我们还可以在代理里面加上自己的控制项。
根据GoF的《Design Patterns》的书,代理模式类图表示为:

图片

真正干活儿的类是RealSubject,具体由DoAction()执行任务, Proxy作为代理提供一个同样的DoAction(),然后调用RealSubject的DoAction().它们都实现Subject interface,Client程序操作的是Subject interface.
时序图表示为:

Client程序调用Proxy, Proxy调用RealSubject。
简单说来,就是在Client程序与真正的服务程序RealSubject之间增加了一个Proxy。这样简化了系统实现。
可能有的同学奇怪了,本来是Client-RealSubject,现在弄成了Client-Proxy-RealSubject,怎么是简化了呢?看起来好像是复杂了呀。
事情是这样的,这种模式有几种应用场景,直接访问RealSubject很麻烦,比如它是外部系统的一个服务,或者在网络远端,或者还需要动态创建或替换真正的实现类,或者要增加权限访问控制逻辑,或者是增加额外的行为。这些场景,使用Proxy,屏蔽这些复杂性,让Client程序简单使用Proxy就获得响应的服务。Client和真正的服务之间decouple解耦了,所以我们称之为简化。
用一个抽象难懂的结构把事情简化,这似乎是科学技术的通行方式。我们学近世代数的时候,学场方程的时候,学有限状态自动机的时候,都能看到这种方式。我们学不明白,而科学家们说我们明明是在简化问题呀!世界太复杂了,所以一个统一的描述就是简化,而要统一就要抽象,这样脱离了直观层面,就会很难弄明白。我个人的经验,就是想想具体应用场景,然后扩展,一层层往上抽象。切莫直接去理解那个抽象的模式,那样做只能让人如坠云雾里。

好,我们举例说明。写程序的人,要落实到代码才算数。先看一个基本反映上面结构的简单程序。
先定义一个服务类接口,代码如下(Subject.java):

public interface Subject {
    String doAction(String name);
}
再定义具体的服务类,代码如下RealSubject.java:
public class RealSubject implements Subject {
    public String doAction(String name) {
        System.out.println("real subject do action "+name);
        return "SUCCESS";
    }
}

再定义代理类,代码如下ProxySubject.java:

public class ProxySubject implements Subject {
    Subject realsubject;
    public ProxySubject() {
        this.realsubject = new RealSubject();
    }
    public String doAction(String name) {
        System.out.println("proxy control");
        String rtnValue = realsubject.doAction(name);
        return "SUCCESS";
    }
}

从这个代理类的代码,我们看得到,它包含了一个真正的服务类,提供同样的接口给外面,内部转头就调用真正的服务类的同样的接口。这个机制跟现实生活中的代理很像,难怪大家都会说代理就是二道贩子扒一层皮。
从这个代码,我们还可以看出通过代理确实隐藏了实现细节。同时,再调用真正的服务接口前后,还可以自己做一点手脚。比如上面程序中的System.out.println("proxy control");这些在代理里面增加的代码,大部分时候都是为了权限控制和附加操作用的。
这个简单的模式就达到了隐藏实现细节以及增加装饰代码的目标。
最后提供一个客户程序使用上面的类,代码如下(Client.java):

public class Client {
    public static void main(String[] args) {
        Subject subject = new ProxySubject();
        subject.doAction("Test");
    }
}

在这个结构下,如果有一天要换一个具体的服务类,Client程序是没有任何变化的,都意识不到变了。整个结构中需要改的只是Proxy类中间包含的具体服务类修改成新的服务类。这真是一个极大的好处。
代理的基本知识就是这么简单。大家自己去试。

以前我讲到这些的时候,有几个人跟我说,用子类继承的方式是不是一样可以达到Proxy的这些效果呢?
我们一起来看看。
不用代理模式,而是写一个子类继承RealSubject,代码如下(RealSubjectSub.java):

public class RealSubjectSub extends RealSubject {
    @Override
    public String doAction(String name) {
        System.out.println("access control");
        String rtnValue = super.doAction(name);
return "SUCCESS";
    }
}

由于没有代理类,客户端程序就直接用子类,代码如下:

public class Client {
    public static void main(String[] args) {
        Subject subject = new RealSubjectSub();
        subject.doAction("Test");
    }
}

通过继承,隐藏实现细节以及增加装饰控制逻辑这两点是可以做到的,但是灵活度不够,需要暴露一些细节给客户程序。
这里可以看出Client程序直接做了new RealSubjectSub(),如果有新的实现类的时候,客户端程序要再次修改。可以看出,灵活性没有Proxy模式高。
不同的模式有不同的应用场景,不会有包打天下的一个模式,我们学习者要试着去理解场景,然后去理解模式,一味死记硬背模式没有多大的帮助。而理解场景,最好的办法就是去动手编程序,只要编的程序足够多,足够大,就一定会碰到某种场景,再动点心思,就会自然理解模式的功用了。

我们还可以更进一步,在Proxy中引入Annotation和Reflection技术,变出花样来。
上面的Proxy程序中,唯一还跟具体实现相关的代码就是this.realsubject = new RealSubject();这一句了。如果我们改成Class.forName("RealSubject")就可以将具体的是实现类配置在外部文件了。
我们还可以再结合Annotation技术,把程序进一步解耦。连创建实际服务类的工作都不由Proxy去做了。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired{
}

Proxy类里面并不直接创建RealSubject实现类,而是声明一个@Autowired annotation。

public class ProxySubject implements Subject {
   @Autowired
    Subject subject;
    public ProxySubject() {
    }
    public String doAction(String name) {
        System.out.println("proxy control");
        String rtnValue = realsubject.doAction(name);
        return "SUCCESS";
    }
}

有注解,自然需要一个程序解释它。我们假设有这么一个框架,扫描ProxySubject,获取注解,看到@Autowired,就从配置表中获取实际的类,实例化之后调用setSubject()进行实例化。
这个结构,就把RealSubject具体的实现类外部化在配置文件中了。
我们模拟写这个框架程序,代码如下(MyTest.java):

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class MyTest {
    public static void main(String[] args)
            throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
        Class<?> clazz = Class.forName("ProxySubject");
        Object obj = clazz.newInstance();
        Field[] flds = clazz.getDeclaredFields();
        if(flds!=null){
            for(Field fld:flds){
                boolean isAutowired = fld.isAnnotationPresent(Autowired.class);
                if(isAutowired){
                    Class<?> concreteClass=Class.forName("RealSubject1");
                    fld.set(obj, concreteClass.newInstance());
                }
            }
        }
        ((ProxySubject)obj).doAction("Test");
    }
}

上面的程序比较简单,就是创建一个Proxy类,找到@Autowired标志的字段,然后创建真实的服务实现类,自动注入之后,调用Proxy类的方法。Proxy不用管具体那个类实现服务,也不用创建这个类。
上述代码没有用到配置文件,所以Class.forName()还是写死的字符串。可以改成从一个外部的beans.xml中获取类的名字:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context">

   <bean id="subject" class="RealSubject1">
   </bean>
</beans>

用这个配置文件,把所有类注册在这里,框架用一个HashMap保存这些bean的信息,实现自动注入。
咦,这不就是IoC吗?对的,我们一下子又碰到了。现代框架,弄去弄来就是那么几个核心,走着走着就会碰到这个老熟人,有时候有“他乡遇故知”的喜悦。
确实Java本身也曾经尝试过提出一套框架,最著名的是EJB,花了很大的功夫,但是由于定位的原因,Sun公司当时主要瞄着Fortune500的大公司,把EJB设计得很复杂,开发和实施成本很大,广大中小规模企业不便于使用,所以不难理解为什么Spring会大行其道成了现代Java体系结构的事实标准。亲身历经EJB的出生,轰轰烈烈推广,式微,眼见Rodd先生在Java阵营里反戈一击,发布影响深远的名篇《Without EJB》,继而Spring成为标准,实在是要叹息一声“沉舟侧畔千帆过,病树前头万木春。”
好,这是IoC框架的内容,不再展开了,未来的讲座会有对框架的更加具体的探讨。

回过头来看Proxy,Java还提供了一个极其强大的功能:动态代理。
我们先了解动态的概念,这得从静态讲起。我们后顾上面的这些代码,发现虽然可以提供多个RealSubject实现类,但是这些类都是事先写好的,我们把这个叫做静态方式。Java提供了运行时自动生成一个新的类的机制。
我们想象一下这个场景。一些类在系统里面运行,执行几个类似的方法,我们想在每个方法执行之前做一点日志或者什么的工作。自然,拿出源代码修改是好办法。但是有时候没有源代码,又想这么做,有没有办法呢?如果我们有一个随着程序运行而动态生成的类,包住这些实际干活的类,提供一个代理机制,不就可以了吗?
话说得很简单,实际上是很难的,关键是Java真的提供了动态生成代理的技术才使得事情可能做得到。我们看一个例子。
还是基于上面的RealSubject1和RealSubject2,它们都实现了Subject interface,都提供doAction()和doSomething()方法。我们的目标就是在调用这些方法的时候,动态引入一些处理逻辑。
先定义一个动态代理,代码如下(SubjectDynamicProxy.java):

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class SubjectDynamicProxy {
    String subjectName = "";
    private Subject subject = null; 

    public SubjectDynamicProxy(String subjectName) {
        this.subjectName = subjectName;
        try {
            this.subject = (Subject) Class.forName(subjectName).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
    public Subject getProxy() {
        return (Subject) Proxy.newProxyInstance(SubjectDynamicProxy.class
                .getClassLoader(), subject.getClass().getInterfaces(),
                new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (method.getName().equals("doAction")) {
                     System.out.println("Access Control");
                     return method.invoke(subject, args); 
                }
                if (method.getName().equals("doSomething")) {
                     System.out.println("other control");
                     return method.invoke(subject, args);
                }
                return null;
            }
        });
    }
}

程序需要解释一下。
先看构造函数,SubjectDynamicProxy(String subjectName),传入了一个名字,这里用的是类的名字,根据名字加载类,新建实例,转成相应的接口:(Subject) Class.forName(subjectName).newInstance()。从这里可以看出,我们示例的这个动态代理是负责代理所有实现了Subject接口的类。
下面我们就要针对接口中的方法,在真正调用前,由动态代理进行一些控制装饰。我们看Java提供的办法生成这个代理类:
(Subject)

Proxy.newProxyInstance(SubjectDynamicProxy.class.getClassLoader(),                                     subject.getClass().getInterfaces(),
                        new InvocationHandler())

这个动态生成代理的办法用起来还是不困难的,Java的reflection包里面有一个Proxy类,负责动态生成代理类,它用到三个参数,classloader, interface,以及一个InvocationHandler。我们的代码主要写在这个InvocationHandler的invoke()中。
Invoke的声明是:invoke(Object proxy, Method method, Object[] args)。Object是代理对象,即newProxyInstance()返回的那个对象,Method是调用的方法名,Object[]是方法的参数。每次调用interface中的方法的时候都会触发invoke。事实上InvocationHandler中也只有这一个方法,我们如果用Java8就可以用Lambda简写上面的代码:

Subject proxyInstance = (Subject) Proxy.newProxyInstance(
          SubjectDynamicProxy.class.getClassLoader(), 
          subject.getClass().getInterfaces(), 
          (proxy, method, methodArgs) -> {
    if (method.getName().equals("doAction")) {
                ...
            }
});

这样实现动态代理,在客户程序调用doAction()的时候,动态代理类就会先接管,执行一些控制装饰操作后再交给实际的类去做。测试一下这个动态代理,

public class DynamicProxyTest {
    public static void main(String[] args) {
        SubjectDynamicProxy proxy = new SubjectDynamicProxy("RealSubject1");
        Subject p = proxy.getProxy();
        String retValue = p.doAction("do action");
        System.out.println(retValue);
        String value = p.doSomething("do something");
        System.out.println(value);
    }
}

运行结果符合预期。在显示do action之前确实出现了代理程序打印的access control。我们的动态效果成功了。
这种神奇的功能,许多人以为是长期发展后出现的,其实是在Java1.3版本就提供了,令人吃惊。

用动态代理,我们就有机会在应用程序员写好的程序上,不动代码,在框架层面就可以抓获这些程序的调用并进行必要的控制装饰,如Log日志,性能监测,事件触发等等。对一些系统要求的常规任务,使用动态代理会省很多事情。当然,这些也要求应用本身比较规范,基于接口式编程,方法名称统一。
熟悉Spring的同学们估计想到AOP了,没错,动态代理技术是实现AOP的有效办法。这些内容我们今天就不展开了,未来会有讲座专门探讨。
以前讲座后,有同学问我,它们看的教科书中将语言就是讲语法,我怎么老是讲别的东西,感觉不是一个语言的讲座。我说是的,我的目的是教你怎么用Java语言写程序,所以有两个重点,一个是语言特性,第二个是怎么写程序。事实上,理解了计算机系统的思维,知道了怎么写程序之后,再转换一种语言就是比较简单的事情了。Bruce Eckel,写经典名著《Thinking in Java》的时候,早就是C++的知名专家。
我们不能固步自封地认为只要掌握基础之道就万事大吉了,“天不变道亦不变”。但是这些编程的基本思维确实能在相对长的时间段在相对广的场合应对日新月异的需求变化。这是编程的本质,而不是浮光一现的泡沫。
人言:“时髦技术如一朝风月,基础知识如万古长空。”

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值