简体   繁体   中英

Java proxy for Autocloseable (Jedis resources)

I am trying to find out whether it is possible to create Java dynamic proxy to automatically close Autocloseable resources without having to remember of embedding such resources with try-resources block.

For example I have a JedisPool that has a getResource method which can be used like that:

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

For now I did something like that:

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;
        }
    }
}

Now each time when I call method on Jedis (JedisCommands) this method is passed to proxy which gets a new client from the pool, executes method and returns this resource to the pool.

It works fine, but when I want to execute multiple methods on client, then for each method resource is taken from pool and returned again (it might be time consuming). Do you have any idea how to improve that?

You would end up with your own "transaction manager" in which you normally would return the object to the pool immediately, but if you had started a "transaction" the object wouldn't be returned to the pool until you've "committed" the "transaction".

Suddenly your problem with using try-with-resources turns into an actual problem due to the use of a hand-crafted custom mechanism.

Using try with resources pros:

  • Language built-in feature
  • Allows you to attach a catch block, and the resources are still released
  • Simple, consistent syntax, so that even if a developer weren't familiar with it, he would see all the Jedis code surrounded by it and (hopefully) think "So this must be the correct way to use this"

Cons:

  • You need to remember to use it

Your suggestion pros (You can tell me if I forget anything):

  • Automatic closing even if the developer doesn't close the resource, preventing a resource leak

Cons:

  • Extra code always means extra places to find bugs in
  • If you don't create a "transaction" mechanism, you may suffer from a performance hit (I'm not familiar with [jr]edis or your project, so I can't say whether it's really an issue or not)
  • If you do create it, you'll have even more extra code which is prone to bugs
  • Syntax is no longer simple, and will be confusing to anyone coming to the project
  • Exception handling becomes more complicated
  • You'll be making all your proxy-calls through reflection (a minor issue, but hey, it's my list ;)
  • Possibly more, depending on what the final implementation will be

If you think I'm not making valid points, please tell me. Otherwise my assertion will remain "you have a 'solution' looking for a problem".

I don't think that this is going into the right direction. After all, developers should get used to handle resources correctly and IDEs/compilers are able to issue warnings when autoclosable resources aren't handled using try(…){}

However, the task of creating a proxy for decorating all invocations and the addition of a way to decorate a batch of multiple action as a whole, is of a general nature, therefore, it has a general solution:

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);
        }
    }
}

Note that the type conversion of a Pool<Jedis> to a JedisPool looked suspicious to me but I didn't change anything in that code as I don't have these classes to verify it.

Now you can use it like

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

The batch execution requires the instance to be a proxy, otherwise an exception is thrown as it's clear that this method cannot guarantee a particular behavior for an unknown instance with an unknown life cycle.

Also, developers now have to be aware of the proxy and the batch execution feature just like they have to be aware of resources and the try(…){} statement when not using a proxy. On the other hand, if they aren't, they lose performance when invoking multiple methods on a proxy without using the batch method, whereas they let resources leak when invoking multiple methods without try(…){} on an actual, non-proxy resource…

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM