Java反射遇到接口

Java反射遇到接口

本文适合有点Java反射基础的同学,在Java反射调用方法时遇到接口参数是一件很蛋疼的事情。
在反射调用方法时需要传参数,像传递基本数据类型进去用就完事,传个对象进去怎么整都没关系,因为你在外部有对象的引用,但 如果需要你传递接口参数,而且这个接口类也是你反射得到的,那怎么拿到接口回调的值呢? 下面通过一个例子告诉你咋整。

场景和需求

场景:假设我是提供方A,有个业务方B,提供ID方C。
其中提供ID方C有如下代码来提供ID。

// IdManager类
public class IdManager {
    private String id;
    private static volatile IdManager mInstance;

    private IdManager() {
    }

    public static IdManager getInstance() {
        if (mInstance == null) {
            synchronized (IdManager.class) {
                if (mInstance == null) {
                    mInstance = new IdManager();
                }
            }
        }
        return mInstance;
    }

    public void setId(String id) {
        this.id = id;
    }

    // 加锁是防止多线程调用造成多次开启子线程
    public synchronized void getId(Callback callback) {
        if (id == null || id.isEmpty()) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // 一些耗时操作...
                    callback.getId(id);
                }
            }).start();
        } else {
            callback.getId(id);
        }
    }
}

// 获取Id的回调接口Callback
public interface Callback {
    void getId(String id);
}

需求是:
如果业务方B接入C(也就是存在IdManager类),那么我方需要获得业务方B设置的Id。
如果业务方B没有接入C(不存在IdManager类),那么我就不获取。

基本分析

懂少许反射的同学肯定认为这太简单了,思路如下:

  1. 使用类的完整路径加载IdManager类
  2. 找到getInstance()方法获取对象(由于该类采用单例模式,避免反射去创建多个对象)
  3. 找到getId()方法
  4. 利用2找到的IdManager对象调用getId()

开始撸代码

public class Main {
    public static void main(String[] args) {
        // 模拟接入方B已经传入id
        IdManager.getInstance().setId("MyId:10086!");
        // 我们的代码
        try {
           // 1. 找不到就catch住抛出的异常(对应第二种未接入的情况)
            Class<?> idManagerClazz = Class.forName("com.grcen.proxy.IdManager");
            // 2. 获取Callback类型
            Class<?> callbackClazz = Class.forName("com.grcen.proxy.Callback");
            // 3. 找到getInstance方法
            Method getInstance = idManagerClazz.getMethod("getInstance");
            // 4.将获取到的Callback类型用来找到getId方法
            Method getId = idManagerClazz.getMethod("getId", callbackClazz);
            // 执行getInstance方法获取到单例对象(参数1:执行该方法的对象。参数2:该方法的参数)
            Object instance = getInstance.invoke(null, null);
            // 利用instance对象执行getId方法获取ID!!
            // getId.invoke(instance,????) -> ???该方法的参数是接口该怎么传呢??
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            // 这里默认处理异常(正式代码别这样搞)
            e.printStackTrace();
        }
    }
}

写到上面出现问号的地方就尴尬了,因为在IdManager类中是通过接口参数来回调id值的。那理所当然我们应该传入应该接口参数,但在该场景下并不知道有没有Callback类,是没法声明接口的。即使找到callbackClazz调用newInstance()来创建对象,那请问怎么知道回调结果啊?而且只能实现接口不能实例化接口。

使用代理

代理顾名思义找个中间商实现接口,在实现的方法中即可拿到回调值。在Java中提供了InvocationHandler接口实现代理。InvocationHandler意为调用处理者。目的很明确:找个类实现我反射拿到的接口,在实现的方法中拿到回调的值。

创建MyHandler类实现InvocationHandler接口

public class MyHandler implements InvocationHandler {
    /**
     * 参数说明:这些参数先知道是个什么意思
     * @param proxy 所代理的那个真实对象 
     * @param method 我们所要调用真实对象的某个方法的Method对象 
     * @param args 调用真实对象某个方法时接受的参数
     * @return 代理执行完方法所返回的对象
     * @throws Throwable 执行过程抛出的各种异常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 这里什么也不干就打印回调的值
        System.out.println(args[0]);
        return null;
    }
}

利用Proxy类关联Callback接口和MyHandler

// Main类完整代码
public class Main {
    public static void main(String[] args) {
        // 模拟接入方已经传入id
        IdManager.getInstance().setId("MyId:10086!");
        // 我们的代码
        try {
            Class<?> idManagerClazz = Class.forName("com.grcen.proxy.IdManager");
            Class<?> callbackClazz = Class.forName("com.grcen.proxy.Callback");
            Method getInstance = idManagerClazz.getMethod("getInstance");
            Method getId = idManagerClazz.getMethod("getId", callbackClazz);
            Object instance = getInstance.invoke(null, null);
            // 新增代码
            MyHandler myHandler = new MyHandler();
            // 参数说明在下文
            Object myCallback = Proxy.newProxyInstance(
                    Main.class.getClassLoader(),
                    new Class[]{callbackClazz},
                    myHandler);
            getId.invoke(instance, myCallback);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

Proxy.newProxyInstance()的参数有:(ClassLoader loader, Class[] interfaces, InvocationHandler h)

  1. 类加载器(不懂的可以看看《深入理解JVM虚拟机》)
  2. 一个Interface类数组,表示要给代理对象实现的接口有哪些
  3. 表示的是当我这个动态代理对象在调用方法的时候,会调用到哪个InvocationHandler的invoke方法。(即将该代理对象与MyHandler关联)

先来看看代码运行结果:
1341563621244_.pic.jpg

再梳理下主要过程,先从getId.invoke(instance,myCallback)来看,执行getId方法,传入执行对象instance,方法所需参数myCallback。然后在getId方法中回调接口,因为myCallback是个代理,它的接口实现在MyHandler,所以最后回调执行的是MyHandler中的invoke方法。前面大致罗列了invoke方法的参数意思,我们来验证一下。

// MyHandler
public class MyHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("invoke参数2真实代理对象所调用的方法" + method);
        System.out.println("invoke参数3调用方法的传入参数为:" + args[0]); // 此处已知只有一个参数
        return null;
    }
}

结果:
1351563622200_.pic.jpg

注意:

  1. 此处我没有将proxy打印出来是因为会出现栈溢出。重申一遍现在的逻辑是 找proxy对象的方法就会到MyHandler中的invoke寻找,那我在invoke中调用proxy,就会出现死循环,方法栈一直入栈。不懂方法栈可以看看《深入JVM虚拟机》
  2. 还记得前面Proxy.newProxyInstance()的参数的参数2吗?可以传入一组Interface类,所以当给代理类实现多个接口是需要利用invoke的参数2做区分

BTW:其实这种动态代理的实现大多数情况下是用在AOP编程中的。

参考文章

  1. java利用反射为接口实例化对象并调用
  2. JAVA 动态代理InvocationHandler和Proxy
  3. java反射与动态代理的理解

你可能感兴趣的:(Java)