封装 Ice 客户端

Ice 客户端的标准写法既繁琐又不利于复用,能否封装一个工具类来获取 Ice 客户端呢?

Ice 客户端的标准写法

Ice.Communicator ic = null;
try {
    // 初始化通信器
    ic = Ice.Util.initialize(args);

    Ice.ObjectPrx base = ic.stringToProxy("Foo:default -p 10000");
    FooServicePrx proxy = FooServiceHelper.checkedCast(base);

    if (proxy == null) {
        throw new Error("Invalid proxy");
    } 
} catch(Exception e) {
    e.printStackTrace();
} finally {
    if (ic != null) {
        ic.destroy();
    }
}

这里存在以下几个问题。

  • Locator 地址不能写死在代码里,它属于共用部分配置
  • Communicator 是重量级全局共享的对象,涉及线程池、Socket 连接等,每次的创建和销毁既影响性能又容易产生泄漏问题
  • Ice Object 的标识符(Identity,如上面的 Foo)与类名有 99% 的可能性是重合的,Identity + Prx 相当于是 Object Proxy 的类名
  • 客户端的代码还是有点多

封装一个 Ice 客户端工具类

因此,我们可以写一个工具类,思路如下:

读取配置文件,获得 Locator 地址并创建全局静态 Communicator 对象。在访问某个 Ice Object Proxy 时,根据其类名反推 Ice Object 的标识符,用反射方法生成Proxy 对象并返回。由于
Proxy 可能被多次使用,因此用 Map 缓存 Proxy,下次调用就不用再生成了。同时启动一个线程,在超过一定的闲置时间后,就关闭 Communicator 对象并移除 Map 中的 Proxy,释放资源。

下面是具体的代码实现:

package com.ucmed.foo.util

import Ice.ObjectPrx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

public class IceClientUtil {
    private static final String locatorKey = "--Ice.Default.Locator";
    private static Logger logger = LoggerFactory.getLogger(IceClientUtil.class);
    private static volatile Ice.Communicator ic = null;
    private static Map<Class, ObjectPrx> cls2PrxMap = new HashMap<>();
    private static volatile long lastAccessTimestamp;
    private static volatile MonitorThread monitorThread;
    private static long idleTimeOutSeconds = 0;
    private static String iceLocator = null;

    // 延迟加载 Communicator
    public static Ice.Communicator getIceCommunicator() {
        if (ic == null) {
            synchronized (IceClientUtil.class) {
                if (ic == null) {
                    ResourceBundle resourceBundle = ResourceBundle.getBundle("iceclient", Locale.ENGLISH);
                    iceLocator = resourceBundle.getString(locatorKey);
                    idleTimeOutSeconds = Integer.parseInt(resourceBundle.getString("idleTimeOutSeconds"));

                    logger.info("Ice client's locator is " + iceLocator + ", proxy cache timeout seconds:" +
                            idleTimeOutSeconds);

                    String[] initParams = new String[]{locatorKey + "=" + iceLocator};
                    ic = Ice.Util.initialize(initParams);

                    createMonitorThread();
                }
            }
        }
        lastAccessTimestamp = System.currentTimeMillis();
        return ic;
    }

    // 创建并开启(守护)线程
    private static void createMonitorThread() {
        monitorThread = new MonitorThread();
        monitorThread.setDaemon(true);
        monitorThread.start();
    }

    // 关闭 Communicator,释放资源
    static void closeCommunicator(boolean removeServiceCache) {
        synchronized (IceClientUtil.class) {
            if (ic != null) {
                safeShutdown();
                monitorThread.interrupt();

                if (removeServiceCache && !cls2PrxMap.isEmpty()) {
                    try {
                        cls2PrxMap.clear();
                    } catch (Exception e) {
                        logger.error("Map 清理失败:" + e.getMessage());
                    }
                }
            }
        }
    }

    private static void safeShutdown() {
        try {
            ic.shutdown();
        } catch (Exception e) {
            logger.error("Ice.Communicator 关闭失败:" + e.getMessage());
        } finally {
            ic.destroy();
            ic = null;
        }
    }

    // 反射创建 Object Proxy
    private static ObjectPrx createIceProxy(Ice.Communicator communicator, Class serviceCls) {
        ObjectPrx proxy;
        String clsName = serviceCls.getName();
        String serviceName = serviceCls.getSimpleName();

        int pos = serviceName.lastIndexOf("Prx");
        if (pos <= 0) {
            throw new IllegalArgumentException("Invalid ObjectPrx class, class name must end with Prx");
        }
        String realSvName = serviceName.substring(0, pos);

        try {
            ObjectPrx base = communicator.stringToProxy(realSvName);
            proxy = (ObjectPrx) Class.forName(clsName + "Helper").newInstance();
            Method method = proxy.getClass().getDeclaredMethod("uncheckedCast", ObjectPrx.class);
            proxy = (ObjectPrx) method.invoke(proxy, base);
            return proxy;
        } catch (Exception e) {
            logger.error("创建 ObjectPrx 失败:" + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    // 用于客户端 Api 获取 Ice 服务实例
    public static ObjectPrx getServicePrx(Class serviceCls) {
        ObjectPrx proxy = cls2PrxMap.get(serviceCls);
        if (proxy != null) {
            lastAccessTimestamp = System.currentTimeMillis();
            return proxy;
        }

        proxy = createIceProxy(getIceCommunicator(), serviceCls);
        cls2PrxMap.put(serviceCls, proxy);

        lastAccessTimestamp = System.currentTimeMillis();

        return proxy;
    }

    static class MonitorThread extends Thread {
        @Override
        public void run() {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(5000L);
                    if (lastAccessTimestamp + idleTimeOutSeconds * 1000L < System.currentTimeMillis()) {
                        closeCommunicator(true);
                    }
                } catch (Exception e) {
                    logger.error("线程中断异常:" + e.getMessage());
                }
            }
        }
    }
}

代码里面加载了一个名为 iceclient 的配置文件进行属性读取。所以,我们在 /src/main/resources 目录下创建 iceclient.properties 文件,内容可如下设置:

--Ice.Default.Locator=IceGrid/Locator:tcp -h 192.168.22.216 -p 4061
idleTimeOutSeconds=300

具体属性值,可根据实际项目情况进行修改。

之后,我们就可以用一行代码,获取到 Ice 服务的客户端(代理)了:

FooServicePrx proxy = (FooServicePrx) IceClientUtil.getServicePrx(FooServicePrx.class);
需要注意的是,这里有一个隐含的契约式编程,即 Ice Object 的 Proxy 名字与在 IceGrid 中定义的 Object Identity 保持之前提到的这种(重合)关系。契约这样的做法,省去了很多不要的代码,这也是契约式编程在很多地方都被或多或少地使用的重要原因。
文章目录
  1. 1. Ice 客户端的标准写法
  2. 2. 封装一个 Ice 客户端工具类
|