【问题标题】:Java proxy for Autocloseable (Jedis resources)Autocloseable 的 Java 代理(Jedis 资源)
【发布时间】:2016-01-28 18:35:32
【问题描述】:

我正在尝试找出是否可以创建 Java 动态代理来自动关闭 Autocloseable 资源,而不必记住使用 try-resources 块嵌入此类资源。

例如,我有一个 JedisPool,它有一个 getResource 方法,可以像这样使用:

try(Jedis jedis = jedisPool.getResource() {
   // use jedis client
}

现在我做了类似的事情:

class JedisProxy implements InvocationHandler {

    private final JedisPool pool;

    public JedisProxy(JedisPool pool) {
        this.pool = pool;
    }

    public static JedisCommands newInstance(Pool<Jedis> pool) {
        return (JedisCommands) java.lang.reflect.Proxy.newProxyInstance(
            JedisCommands.class.getClassLoader(),
            new Class[] { JedisCommands.class },
            new JedisProxy(pool));
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try (Jedis client = pool.getResource()) {
            return method.invoke(client, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (Exception e) {
            throw e;
        }
    }
}

现在,每次我在 Jedis (JedisCommands) 上调用方法时,此方法都会传递给代理,代理会从池中获取新客户端,执行方法并将此资源返回到池中。

它工作正常,但是当我想在客户端上执行多个方法时,每个方法的资源都从池中取出并再次返回(这可能很耗时)。你知道如何改进吗?

【问题讨论】:

    标签: java proxy java-8 jedis autocloseable


    【解决方案1】:

    您最终会拥有自己的“事务管理器”,在该管理器中,您通常会立即将对象返回到池中,但如果您启动了“事务”,则在您完成之前不会将对象返回到池中“提交”“交易”。

    由于使用了手工定制的机制,您使用 try-with-resources 的问题突然变成了实际问题。

    与资源专家一起使用 try:

    • 语言内置功能
    • 允许你附加一个 catch 块,并且资源仍然被释放
    • 简单、一致的语法,因此即使开发人员不熟悉它,他也会看到所有 Jedis 代码被它包围并(希望)认为“所以这一定是使用它的正确方法”

    缺点:

    • 你需要记住使用它

    您的建议专家(如果我忘记了什么,您可以告诉我):

    • 即使开发者不关闭资源也会自动关闭,防止资源泄漏

    缺点:

    • 额外的代码总是意味着有额外的地方可以找到错误
    • 如果您不创建“事务”机制,您可能会受到性能影响(我不熟悉 [jr]edis 或您的项目,所以我不能说是否这真的是一个问题)
    • 如果您创建它,您将拥有更多容易出现错误的额外代码
    • 语法不再简单,任何参与该项目的人都会感到困惑
    • 异常处理变得更加复杂
    • 您将通过反射进行所有代理调用(这是一个小问题,但嘿,这是我的清单;)
    • 可能更多,具体取决于最终的实现方式

    如果您认为我没有提出有效的观点,请告诉我。否则我的断言将仍然是“您有一个寻找问题的'解决方案'”。

    【讨论】:

    • 这不是一个复杂的代码。通常jedis客户端定义了几十种方法。我创建了一个适配器,它只定义了 4 种方法来解决我的问题。在每种方法中,我只使用一次 try-with-resources。不幸的是,我担心任何尝试编写第 5 种方法的人都会忘记 try-with-resources 并且在每个方法调用池之后都会变小。
    • 编写事务管理器而不是使用内置机制不是复杂的代码吗?你害怕下一个人会忘记吗?也许你应该用大写字母写一个评论:“记得在使用后关闭资源”。这将是一个更有效的方法来处理您的担忧。
    • @Konrad 我试图在上面的编辑中阐明我的观点。如果您仍然觉得自己有一个实际案例,我祝您好运。
    【解决方案2】:

    我不认为这是朝着正确的方向发展。毕竟,开发人员应该习惯于正确处理资源,并且 IDE/编译器能够在未使用 try(…){} 处理自动关闭资源时发出警告...

    但是,创建一个代理来装饰所有调用以及添加一种方法来装饰一批多个动作作为一个整体的任务具有一般性,因此,它有一个通用的解决方案:

    class JedisProxy implements InvocationHandler {
    
        private final JedisPool pool;
    
        public JedisProxy(JedisPool pool) {
            this.pool = pool;
        }
    
        public static JedisCommands newInstance(Pool<Jedis> pool) {
            return (JedisCommands) java.lang.reflect.Proxy.newProxyInstance(
                JedisCommands.class.getClassLoader(),
                new Class[] { JedisCommands.class },
                new JedisProxy(pool));
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try (Jedis client = pool.getResource()) {
                return method.invoke(client, args);
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }
        }
        public static void executeBatch(JedisCommands c, Consumer<JedisCommands> action) {
            InvocationHandler ih = Proxy.getInvocationHandler(c);
            if(!(ih instanceof JedisProxy))
                throw new IllegalArgumentException();
            try(JedisCommands actual=((JedisProxy)ih).pool.getResource()) {
                action.accept(actual);
            }
        }
        public static <R> R executeBatch(JedisCommands c, Function<JedisCommands,R> action){
            InvocationHandler ih = Proxy.getInvocationHandler(c);
            if(!(ih instanceof JedisProxy))
                throw new IllegalArgumentException();
            try(JedisCommands actual=((JedisProxy)ih).pool.getResource()) {
                return action.apply(actual);
            }
        }
    }
    

    请注意,Pool&lt;Jedis&gt;JedisPool 的类型转换对我来说看起来很可疑,但我没有更改该代码中的任何内容,因为我没有这些类来验证它。

    现在你可以像这样使用它了

    JedisCommands c=JedisProxy.newInstance(pool);
    
    c.someAction();// aquire-someaction-close
    
    JedisProxy.executeBatch(c, jedi-> {
        jedi.someAction();
        jedi.anotherAction();
    }); // aquire-someaction-anotherAction-close
    
    ResultType foo = JedisProxy.executeBatch(c, jedi-> {
        jedi.someAction();
        return jedi.someActionReturningValue(…);
    }); // aquire-someaction-someActionReturningValue-close-return the value
    

    批量执行需要实例为代理,否则会抛出异常,因为很明显该方法不能保证生命周期未知的未知实例的特定行为。

    此外,开发人员现在必须了解代理和批处理执行功能,就像他们在不使用代理时必须了解资源和 try(…){} 语句一样。另一方面,如果不是,它们会在不使用批处理方法的情况下在代理上调用多个方法时失去性能,而在没有try(…){} 在实际的非代理资源上调用多个方法时会导致资源泄漏……

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-05-24
      • 2018-06-19
      • 2015-08-13
      • 1970-01-01
      • 1970-01-01
      • 2019-02-22
      • 2017-11-22
      • 2018-09-16
      相关资源
      最近更新 更多