1

I have a public abstract class java.nio.ByteBuffer instance which is actually an instance of private class java.nio.HeapByteBuffer and I need to make a proxy object which would call some invocation method handler to check access permissions and then call the invoked method on the actual instance.

The problem is that the java.nio.ByteBuffer class has only private constructors and also has some final methods, thus I can not create proxy instances with javassist.util.proxy.ProxyFactory class.

So, how can I make a proxy object to control the invocation of a java.nio.ByteBuffer instance including those final methods invocation?

5
  • At least not using javassist and not using cglib. Could you probably explain in more details why do you need this? ByteBuffer is a part of NIO that was introduced partially to be very fast IO. Using proxy (and therefore reflection calls) will kill the performance. So, again: why? Commented Sep 16, 2014 at 8:53
  • @AlexR, the proxy object is not for a production code execution, it is to be created when the application is running in special debugging mode. I have some foreign legacy code which caches instances of classes which use ByteBuffer instances stored within their inner fields and it turns out that those are still used from somewhere after being placed back into the cache. I see that javassist and cglib can not handle such case. But what can? Commented Sep 16, 2014 at 9:17
  • I have examined javassist CtClass.addConstructor and ByteBuddy briefly, but they are not easy to figure out the proxy object creation part. Some sample code might help but I can't find any. Commented Sep 16, 2014 at 9:18
  • @AlexR Proxies are not slow in Java, thanks to the JIT compiler. Reflective calls are an easy optimization target. Commented Sep 16, 2014 at 11:04
  • @KoichiSenada Neither Byte Buddy, nor Javassist are concerned with instance creation. They are both meant for class creation. Objenesis is however concerned with object creation where this library almost became an industry standard for mocking frameworks. Commented Sep 16, 2014 at 11:11

3 Answers 3

3

Please be aware that I am presenting a solution based on my own (FOSS) framework Byte Buddy which is however already mentioned as a potential solution in one of the comments.

Here is a simple proxy approach which creates a subclass. First, we introduce a type for creating proxies for ByteBuffers:

interface ByteBufferProxy {
  ByteBuffer getOriginal();
  void setOriginal(ByteBuffer byteBuffer);
}

Furthermore, we need to introduce an interceptor to use with a MethodDelegation:

class Interceptor {
  @RuntimeType
  public static Object intercept(@Origin(cacheMethod = true) Method method,
                                 @This ByteBufferProxy proxy,
                                 @AllArguments Object[] arguments) 
                                     throws Exception {
    // Do stuff here such as:
    System.out.println("Calling " + method + " on " + proxy.getOriginal());
    return method.invoke(proxy.getOriginal(), arguments);
  }
}

This interceptor is capable of intercepting any method as the @RuntimeType casts the return type in case that it does not fit the Object signature. As you are merely delegating, you are safe. Plase read the documentation for details. As you can see from the annotations, this interceptor is only applicable for instances of ByteBufferProxy. Bases on this assumption, we want to:

  1. Create a subclass of ByteBuffer.
  2. Add a field to store the original (proxied) instance.
  3. Implement ByteBufferProxy and implement the interface methods to access the field for the stored instance.
  4. Override all other methods to call the interceptor that we defined above.

This we can do as follows:

@Test
public void testProxyExample() throws Exception {

  // Create proxy type.
  Class<? extends ByteBuffer> proxyType = new ByteBuddy()
    .subclass(ByteBuffer.class)
    .method(any()).intercept(MethodDelegation.to(Interceptor.class))
    .defineField("original", ByteBuffer.class, Visibility.PRIVATE)
    .implement(ByteBufferProxy.class).intercept(FieldAccessor.ofBeanProperty())
    .make()
    .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
    .getLoaded();

    // Create fake constructor, works only on HotSpot. (Use Objenesis!)
    Constructor<? extends ByteBufferProxy> constructor = ReflectionFactory
      .getReflectionFactory()
      .newConstructorForSerialization(proxyType, 
                                      Object.class.getDeclaredConstructor());

    // Create a random instance which we want to proxy.
    ByteBuffer byteBuffer = ByteBuffer.allocate(42);

    // Create a proxy and set its proxied instance.
    ByteBufferProxy proxy = constructor.newInstance();
    proxy.setOriginal(byteBuffer);

    // Example: demonstrates interception.
    ((ByteBuffer) proxy).get();
}

final methods are obviously not intercepted. However as the final methods in ByteBuffer only serve as convenience methods (e.g. put(byte[]) calls put(byte[],int,int) with the additional arguments 0 and the array length), you are still able to intercept any method invocation eventually as these "most general" methods are still overridable. You could even trace the original invocation via Thread.currentCallStack().

Byte Buddy normally copies all constructors of its super class if you do not specify another ConstructorStrategy. With no accessible constructor, it simply creates a class without constructors what is perfectly legal in the Java class file format. You cannot define a constructor because, by definition, this constructor would need to call another constructor what is impossible. If you defined a constructor without this property, you would get a VerifierError as long as you do not disable the verifier altogether (what is a terrible solution as it makes Java intrinsically unsafe to run).

Instead, for instantiation, we call a popular trick that is used by many mocking frameworks but which requires an internal call into the JVM. Note that you should probably use a library such as Objenesis instead of directly using the ReflectionFactory because Objenesis is more robust when code is run on a different JVM than HotSpot. Also, rather use this in non-prduction code. Do however not worry about performance. When using a reflective Method that can be cached by Byte Buddy for you (via cacheMethod = true), the just-in-time compiler takes care of the rest and there is basically no performance overhead (see the benchmark on bytebuddy.net for details.) While reflective lookup is expensive, reflective invocation is not.

I just released Byte Buddy version 0.3 and I am currently working on documentation. In Byte Buddy 0.4, I plan to introduce an agent builder which allows you to redefine classes during load-time without knowing a thing about agents or byte code.

Sign up to request clarification or add additional context in comments.

4 Comments

I have combined everything into a TestNG class and the load method throws IllegalAccessException. java.lang.IllegalAccessError: class net.bytebuddy.renamed.java.nio.ByteBuffer$ByteBuddy$100644077 cannot access its superinterface ByteBuddyTest$ByteBufferProxy
I assume that ByteBufferProxy is package-private in your solution? This won't work unless you define the created proxyType to be in the same package as ByteBufferProxy. Alternatively, make it public. Same goes for the interceptor. Also, from the default type naming pattern, I assume that you use version 0.2.1 - rather use 0.3, it fixes some minor issues.
it worked and I have also replaced the instantiation with ObjenesisStd instantiator which keeps working, too. But whether is that possible to intercept methods the way it is done with javassist to call some method invocation handler instance? The ProxyObject.setHandler method accepts a handler which can be a custom MethodHandler instance constructed with the original object passed to it.
Javassist does effectively nothing else than what I have written down for you. However, Javassist takes a lot of flexibility from you by requiring an interceptor to be an instance of the fixed MethodHandler type. With Byte Buddy, you can use any POJO and even static methods as shown above. Instead of using ProxyObject.setHandler, you can simply use your own type such as ByteBufferProxy. Other than Javassist, Byte Buddy does not maintain a class cache. I explained why in this answer: stackoverflow.com/questions/23732236/…
1

I can suggest you 2 solutions.

First, simple, not universal, but probably useful for you.

As far as I can see ByteBuffer has several package-private constructors that allow its subclassing and the following final methods:

public final ByteBuffer put(byte[] src) {
public final boolean hasArray() {
public final byte[] array() {
public final int arrayOffset() {
public final ByteOrder order() {

ByteBuffer extends Buffer that declares some of these methods:

public final boolean hasArray() {
public final Object array() {
public final int arrayOffset() {

As you can see, put() and order() are absent here, return type of array() is a little bit confusing, but still can be used. So, if you use only these 3 methods you can subclass Buffer and create universal wrapper that wraps any other Buffer including ByteBuffers. If you want you can use javaassist's proxy although IMHO it is not necessarily here.

Second, more universal but more tricky solution. You can create agent that removes final modifiers from speicific class (ByteBuffer in your case) during class loading. Then you can create javassist proxy.

Variation of second solution is following. Copy ByteBuffer soruce code to separate project. Remove final modifiers and compile it. Then push it into bootstrap classpath. This solutions is probably easier than second.

Good luck anyway.

Comments

0

Thanks to @raphw I have managed to make a proxy object construction class which makes a proxy for java.nio.ByteBuffer but that class has final methods which I can not overcome and they are extensively used in the required code, those final methods are Buffer.remaining() and Buffer.hasRemaining(), thus they just can not be proxy mapped.

But I would like to share the classes I have made, just as a report.

public final class CacheReusableCheckerUtils {
        private static ByteBuddy buddy = new ByteBuddy();
        private static Objenesis objenesis = new ObjenesisStd();

        public static <T> T createChecker(T object) {
            return createChecker(new CacheReusableCheckerInterceptor<>(object));
        }

        public static <T> T createChecker(CacheReusableCheckerInterceptor<T> interceptor) {
            return objenesis.getInstantiatorOf(createCheckerClass(interceptor)).newInstance();
        }

        private static <T> Class<? extends T> createCheckerClass(CacheReusableCheckerInterceptor<T> interceptor) {
            Class<T> objectClass = interceptor.getObjectClass();
            Builder<? extends T> builder = buddy.subclass(objectClass);
            builder = builder.implement(CacheReusableChecker.class).intercept(StubMethod.INSTANCE);
            builder = builder.method(MethodMatchers.any()).intercept(MethodDelegation.to(interceptor));
            return builder.make().load(getClassLoader(objectClass, interceptor), Default.WRAPPER).getLoaded();
        }

        private static <T> ClassLoader getClassLoader(Class<T> objectClass, CacheReusableCheckerInterceptor<T> interceptor) {
            ClassLoader classLoader = objectClass.getClassLoader();
            if (classLoader == null) {
                return interceptor.getClass().getClassLoader();
            } else {
                return classLoader;
            }
        }
    }

public class CacheReusableCheckerInterceptor<T> {
    private T object;
    private boolean allowAccess;
    private Throwable denyThrowable;

    public CacheReusableCheckerInterceptor(@NotNull T object) {
        this.object = object;
    }

    @SuppressWarnings("unchecked")
    public Class<T> getObjectClass() {
        return (Class<T>) object.getClass();
    }

    @RuntimeType
    public final Object intercept(@Origin(cacheMethod = true) Method method, @This T proxy, @AllArguments Object[] arguments) {
        try {
            switch (method.getName()) {
                case "allowAccess":
                    allowAccess();
                    return null;
                case "denyAccess":
                    denyAccess();
                    return null;
                default:
                    return invokeMethod(method, arguments);
            }
        } catch (Exception e) {
            throw new CacheReusableCheckerException(method, object, proxy, e);
        }
    }

    private Object invokeMethod(Method method, Object[] arguments) throws IllegalAccessException, InvocationTargetException {
        checkMethodAccess(method.getName());
        return method.invoke(object, arguments);
    }

    private void allowAccess() {
        if (allowAccess) {
            error("double use");
        }
        allowAccess = true;
        onAccessAllowedAfter(object);
    }

    private void denyAccess() {
        if (!allowAccess) {
            error("double free");
        }
        onAccessDeniedBefore(object);
        allowAccess = false;
        denyThrowable = new Throwable();
    }

    private void checkMethodAccess(String name) {
        if (!allowAccess) {
            switch (name) {
                case "hash":
                case "equals":
                case "toString":
                case "finalize":
                    break;
                default:
                    error("use after free");
            }
        }
    }

    private void error(String message) {
        throw new CacheReusableCheckerException(message, denyThrowable);
    }

    protected void onAccessAllowedAfter(T object) {
    }

    protected void onAccessDeniedBefore(T object) {
    }
}

public interface CacheReusableChecker {

    void allowAccess();

    void denyAccess();

}

1 Comment

Thanks for sharing this. Look out for Byte Buddy version 0.4 which comes with a agent builder which will allow you to generate such proxies by redefining classes during class loading.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.