最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

反射3

网站源码admin1浏览0评论

反射3

编写程序的时候,我们肯定希望把注意力全部放到需要落地的业务功能上,可是很多时候事情并没有那么简单。比如写一个接口,需要接收一个特定的请求并进行处理,那除了这个接口的本身的功能外,还有很多杂七杂八的处理:请求要不要安全验证、要不要鉴权、请求处理完了以后要不要进行收尾工作等等。

以Person类所实现的接口Eater为例:按照当前的实现方法,Person对其的实现是“拿起来就吃”,现在我们要给吃这个动作加上一个前置和一个收尾工作(为了表达方便,在后文中这种操作统称为包装操作)。

代码语言:txt复制
前置:判断一下这个玩意能不能吃。
收尾:洗碗。

实现一:直接写进eat()中。

修改当前Person的eater实现,将eat()的实现代码改为如下:

代码语言:java复制
public void eat() {
        System.out.println("check the food");
        System.out.println("Person eat now");
        System.out.println("wash the dishes");
    }

Main的实现代码如下:

代码语言:java复制
package com.sunhw.main;

import com.sunhw.user.Person;

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.eat();
    }
}
代码语言:txt复制
输出:
check the food
Person eat now
wash the dishes

这种方法的好处很明显:简单,没有任何额外处理和复杂机制,但是缺点很大。

第一个缺点,业务实现和包装操作全部写到一起,增加了代码的复杂度。本次例子中很特殊,包装操作非常简单:两个out输出而已,但是在更普遍的情况下包装操作的逻辑可比这个复杂的多。如果之后需求更新,比如eat()的业务实现要更新,或者eat()的包装策略有调整,或者干脆两个一起来。那到时候面对这一大坨代码,修改和调试起来是一件非常酸爽的事情。

第二个缺点,eat()方法是接口Eater中定义的,而实现该接口的恐怕不只有Person类,比如写一个Cat类,它也需要实现Eater接口。那Cat类实现Eater接口要不要添加上包装操作?添加的话,和Person类的包装是否一样(猫不会洗碗......吧)?如果有一天包装操作需要调整,那哪些类的Eater实现要改?哪些不要改?你要人工找出所有实现了Eater接口的类,人工判断这些类符合哪种调整策略,人工对这些类的每一个Eater接口实现函数进行手工调整,然后再测试。这个过程中你不能漏掉任何一个类,也不能漏掉任何一个类的任何一个接口实现,然后每个类的每个接口实现函数中的每个修改都不能出错,尽管它们很多都是重复的。结合一下第一个缺点,当代码量大了起来,这也是一个非常酸爽的事情。

实现二:静态代理

我们可以编写一个专门的类来专门实现这些包装操作,针对Eater接口,专门实现一个EaterProxy类用于代理实现Eater接口。

代码语言:java复制
package com.sunhw.user;

public class EaterProxy implements Eater {
    private Eater eater;

    public EaterProxy(Eater eater) {
        this.eater = eater;
    }

    @Override
    public void eat() {
        before();
        eater.eat();
        after();
    }

    private void before() {
        System.out.println("check the food");
    }

    private void after() {
        System.out.println("wash the dishes");
    }
}

Main中的实现方法如下:

代码语言:java复制
package com.sunhw.main;

import com.sunhw.user.EaterProxy;
import com.sunhw.user.Person;

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        EaterProxy ep = new EaterProxy(p);

        ep.eat();
    }
}

这种方法的好处是在两个类的分工上更加明确:PersonProxy类不关心Person中的eat()到底是怎么实现的,它的关注重点只是在eat()的包装方法本身。这种方式可以把eat()的实现切割为两个切面:一个只关心eat()的业务落地,而不用关心任何非业务的部分;一个只关心和业务无关的包装操作,而不在乎业务的具体实现。

对其中的三个角色(只关心业务具体实现的Person、只关心包装操作的PersonProxy、只关心如何使用eat()的消费方Main)实现了一定程度的隔离。当其中一方,如包装操作调整时,另外两方,业务实现和消费者Main类,都是完全无感,在一定程度上实现了代码的解耦合。

实现三:动态代理

刚才的实现方法是静态代理,即提前把代理类写好,然后使用的时候,直接指定对应要用的代理类即可实现功能。优点之前已经说过了,缺点呢?对每一个接口,你都要独立实现至少一个(可能针对同一个接口的不同实现类,代理也不一样)专门的代理类才能进行使用。

考虑一下这样一种场景:计算执行时间。计算时间这个操作,操作很统一,也很简单:方法执行前记录下时间戳,方法执行后记录时间戳并减去执行前的时间戳。几乎每个接口的每个方法都能使用,而这正是麻烦的地方:你要为每个接口都专门写一个“计算时间代理类”吗?不仅是对现有的接口,还有未来可能会新出现的任何接口?那如果有一个类实现两个接口,两个接口的方法都要计时,怎么代理?说实话,也不是不能写,但是会特别麻烦,而且不好维护。

还有一种代理是动态的,即接口的代理实现类并没有预先写好,而是在程序运行的过程中生成的。还是以刚才说的计时为例,这次具体一点,代理FasterEater接口,代理内容为对其方法计时(学生党可真惨,吃饭还要计时)。我们编写一个实现计时逻辑的类TimerHandler,以下直接给出代码:

代码语言:java复制
package com.sunhw.user;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class TimerHandler implements InvocationHandler {

    Object target;

    public TimerHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result =  method.invoke(target);
        after();
        return result;
    }

    private void before() {
        System.out.println("start time is "+System.nanoTime());
    }

    private void after() {
        System.out.println("finish time is "+System.nanoTime());
    }
}

代码中target变量用于表示需要被代理的对象,before和after代码也非常简单,不再解释。有意思的是,虽然说是在代理FasterEater接口,但是在这个代码中,代理类TimeHandler并没有实现FasterEater接口,它实现了一个非常奇怪的接口:InvocationHandler。这个接口中只有一个方法:

代码语言:java复制
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

解释这三个参数,proxy表示代理对象(是代理对象,不是被代理对象),method表示要代理的方法,args表示调用method方法要传入的参数。这个函数的效果是,在之后的使用中,当需要通过代理调用method方法时,实际运行的是invoke方法。TimerHandler的好处是,它并不和某个真正要代理的接口绑定,任何一个接口,都可以通过TimerHandler实现代理计时。

处理类写好了,怎么让它代理FasterEater接口呢?这里还需要另一个类的配合。下面先给出Main类的代码,然后再来慢慢解释。

代码语言:java复制
package com.sunhw.main;

import com.sunhw.user.FasterEater;
import com.sunhw.user.Student;
import com.sunhw.user.TimerHandler;

import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        Student student = new Student();
        TimerHandler th = new TimerHandler(student);
        // 生成代理类,并用接口引用它
        FasterEater fe = (FasterEater)Proxy.newProxyInstance(
                th.getClass().getClassLoader(),
                new Class[]{FasterEater.class},
                th);
        // 调用代理类的接口方法
        fe.eat();
        // 输出代理类的类名看看
        System.out.println(fe.getClass().getName());
    }
}
代码语言:txt复制
输出:
start time is 615004073845600
Person eat now
finish time is 615004080818900
jdk.proxy1.$Proxy0

这里用到了Proxy类的newProxyInstance方法,其函数声明如下:

代码语言:java复制
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)

loader为类加载器对象,决定了生成的代理类应该由哪个类加载器加载(类加载器讲起来可太长了,这里不解释,反正就先用和代理处理方法类同一个就行了。);interfaces是一个接口数组,表示这个生成的代理类要代理哪些接口。对,它是数组,意味着你可以将你编写的代理逻辑同时用于多个接口(当然,它既然能代理接口,说明这个代理类肯定也实现了这些接口。这是你可以用其中的接口引用代理类的基础);最后h不用说了,就是我们刚才写的具体的代理实现。

当代码运行到Proxy.newProxyInstance方法时,JVM会直接生成对FasterEater的代理实现。相比于静态代理,动态代理由于并没有把代理关系写死在代码中,而显得更加灵活(TimerHandler可以代理任何接口,不一定非要是FasterEater接口);并且代理的逻辑只需要编写一次而非重复编写,减小了代码冗余,提高了代码质量。

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论