5

I need a way to access fields in a reflective nature without the performance hits from standard reflection. I have figured out how to do this with methods/constructors via LambdaMetaFactory using a privileged lookup handle, however, I can't seem to figure out how to gain field access.

I thought I could generate an inner class via something like javaassist which should theoretically have access to that field but that did not work out, throwing an IllegalAccessError.

If I could redefine the class the task would be trivial as I could generate getter/setter methods. However, for the project I am working on, I am unable to use an agent as it would need to be loaded at runtime and I would have to dynamically import the attach api from tools.

Could anyone guide me in the right direction here? I've looked into how LambdaMetaFactory generates it's interface for methods and tried to mirror that with fields with no success. Is there something internally different with fields and methods that makes this task impossible without redefinition?

harrisondev
  • 127
  • 7
  • 1
    Have you looked into [`VarHandle`](https://docs.oracle.com/javase/9/docs/api/java/lang/invoke/VarHandle.html)? – Louis Wasserman Aug 14 '20 at 21:30
  • 1
    ByteBuddy can redefine a loaded class so adding a getter for each private field is not that hard (compared to anything with BB which is quite tough to start with!). See "Reloading a class" https://bytebuddy.net/#/tutorial – drekbour Aug 14 '20 at 22:04
  • @LouisWasserman I have, unfortunately it was introduced in java 9 and I'm really looking to stick to java 8+ – harrisondev Aug 14 '20 at 22:38
  • @drekbour In my testing ByteBuddy (or any agent/byte code manipulation framework) can't add/remove methods unless you are using dcevm. `java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method` – harrisondev Aug 14 '20 at 22:39

2 Answers2

2

In case of OpendJDK (and JDKs built atop it), the LambdaMetaFactory generates a mostly ordinary class file (just accessing private member(s) of another class) and uses a special method in sun.misc.Unsafe, to create an anonymous class.

Creating a similar class file accessing a field, is straight-forward and creating an anonymous class with it, does work, as can be demonstrated with the following quick&dirty program:

public class Generator {
    public static void main(String[] args) throws Throwable {
        ToIntFunction<Thread> ft=generateIntFieldAccessor(Thread.class, "threadStatus");
        System.out.println(ft.applyAsInt(Thread.currentThread()));
    }

    private static <X> ToIntFunction<X> generateIntFieldAccessor(
        Class<? super X> c, String name) throws Throwable {

        byte[] code = Generator.generateIntReaderCode(c.getDeclaredField(name));
        Class<?> unsafe = Class.forName("sun.misc.Unsafe");
        Field u = unsafe.getDeclaredField("theUnsafe");
        u.setAccessible(true);
        Object theUnsafe = u.get(null);
        Class<ToIntFunction<X>> gen = (Class<ToIntFunction<X>>)
            MethodHandles.publicLookup().bind(theUnsafe, "defineAnonymousClass",
                 MethodType.methodType(
                     Class.class, Class.class, byte[].class, Object[].class))
                .invokeExact(c, code, (Object[])null);
        return gen.getConstructor().newInstance();
    }

    private static final String HEAD = "Êþº¾\0\0\0004\0\24\7\0\21\7\0\t\7\0\n\7\0\22"
        + "\n\0\2\0\6\f\0\13\0\f\t\0\4\0\b\f\0\23\0\20\1\0\20java/lang/Object\1\0\40"
        + "java/util/function/ToIntFunction\1\0\6<init>\1\0\3()V\1\0\4Code\1\0\n"
        + "applyAsInt\1\0\25(Ljava/lang/Object;)I\1\0\1I";
    private static final String TAIL = "\0001\0\1\0\2\0\1\0\3\0\0\0\2\0\1\0\13\0\f\0"
        + "\1\0\r\0\0\0\21\0\1\0\1\0\0\0\5*·\0\5±\0\0\0\0\0\21\0\16\0\17\0\1\0\r\0\0"
        + "\0\24\0\1\0\2\0\0\0\b+À\0\4´\0\7¬\0\0\0\0\0\0";

    public static byte[] generateIntReaderCode(Field f) {
        return new ByteArrayOutputStream(HEAD.length() + TAIL.length() + 100) {
            @SuppressWarnings("deprecation") byte[] get() {
                HEAD.getBytes(0, count = HEAD.length(), buf, 0);
                try(DataOutputStream dos = new DataOutputStream(this)) {
                    String decl = f.getDeclaringClass().getName().replace('.', '/');
                    dos.writeByte(1); dos.writeUTF(decl+"$"+f.getName()+"$access");
                    dos.writeByte(1); dos.writeUTF(decl);
                    dos.writeByte(1); dos.writeUTF(f.getName());
                } catch (IOException ex) {
                    throw new UncheckedIOException(ex);
                }
                int dynSize = count;
                byte[] result = Arrays.copyOf(buf, dynSize + TAIL.length());
                TAIL.getBytes(0, TAIL.length(), result, dynSize);
                return result;
            }
        }.get();
    }
}

Demo on Ideone

Of course, for production code you should better resort to one of the commonly used code generation libraries, to have maintainable factory code. E.g., OpenJDK’s LambdaMetaFactory uses the ASM library under the hood.

If your attempt to implement a similar solution failed, you have to post what you’ve tried, so we can help identifying the problem. But perhaps, knowing that it is possible in general, does already help you.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • of course _straight-forward_ ... :) nevertheless beautiful. – Eugene Aug 25 '20 at 19:35
  • 1
    @Eugene I didn’t meant to say that my code was straight-forward. I rather meant that it’s clear how the factory code for whatever code generation library you use should look like, so I skipped that part and inlined pre-generated code. To keep the example code short and runnable on Ideone. – Holger Aug 26 '20 at 07:53
  • I worked out my similar approach, the issue was that I was trying to implement an interface not defined in the same ClassLoader. I have published my library to speed up reflection at https://github.com/Nesaak/NoReflection/ – harrisondev Aug 26 '20 at 15:34
  • 1
    ClassLoader reachability can indeed make the thing nontrivial. It could work with Unsafe via constant pool patching, but it’s not clear whether this will endure. There are plans to add an official API for anonymous classes before Unsafe will be removed, but it’s not clear whether this will support constant pool patching (I’d say, it’s rather unlikely). – Holger Aug 26 '20 at 15:40
0

You could try runtime code generation using Byte Buddy or Javassist, but this will only provide a performance gain if you need to access the same field on different objects many times. Otherwise the overhead for the code generation will likely be higher than that of using reflection.

If you think runtime code generation might work for your situation, have a look at https://github.com/raner/projo, specifically the code in projo-runtime-code-generation/src/main/java/pro/projo/internal/rcg. Note that that code actually generates the fields as well, it does not use existing fields of existing classes, so it's not 100% what you need but might give you a pointer in the right direction.

raner
  • 1,175
  • 1
  • 11
  • 21
  • I'm definitely going to be using some sort of code generation, as it's how I am currently approaching methods. I tried using JavaAssist to generate inner classes but as stated in my post "I thought I could generate an inner class via something like javaassist which should theoretically have access to that field but that did not work out, throwing an IllegalAccessError". My main problem has been gaining access through private/protected modifiers. – harrisondev Aug 14 '20 at 22:41
  • "Inner classes" are not really a thing at the JVM level. Even though it may seem that a nested class has access to the private fields of its enclosing class, in reality, at the JVM-level there are synthetic accessor methods that the compiler generates on the outer/enclosing (!) class. So, you could instrument the outer class to provide similar accessors. This link might be helpful to understand synthetic accessors: https://blog.gypsyengineer.com/en/security/accessing-private-fields-with-synthetic-methods-in-java.html – raner Aug 14 '20 at 23:05