Search code examples
javabytecodejava-bytecode-asm

Java ASM method override check


I have a problem with method override checks. I can detect simple override relations, but if the parent class has generics and the abstract method uses type parameters (return value/args), my code breaks down because the method description is not equal to the checked method.

Example:

public interface ISetting<T> {

public T method();

}
public class Setting implements ISetting<Integer> {

public Integer method() {
//Something
}

}

In ISetting, the method description is ()Ljava/lang/Object; and in Setting, the method description is ()Ljava/lang/Integer;

How I can check this Override ?

On my head no thoughts come, how I can make this >~< All ideas which come to my head are bad (example: ignore check on desc, but overload method just break this idea)


Solution

  • Note that your issue does not only apply to generic supertype. You can also override a method with a more specific return type, with no Generics involved, e.g.

    interface SomeInterface {
        Object method();
    }
    class SomeImplementation implements SomeInterface {
        @Override
        public Integer method() {
            return null;
        }
    }
    

    You have to understand the concept of bridge methods.

    A bridge method performs the task of overriding a method on the byte code level, having exactly the same parameter types and return type as the overridden method, and delegates to the actual implementation method.

    Since the bridge method only consists of this invocation instruction, some type casts if required, and the return instruction, it is easy to parse such a method to find the actual method it belongs to, without dealing with the complex rules of the Generic type system.

    Using, the following helper classes

    record MethodSignature(String name, String desc) {}
    
    record MethodInfo(int access, String owner, String name, String desc) {
        MethodSignature signature() {
            return new MethodSignature(name, desc);
        }
    }
    
    final class MethodAndBridges {
        MethodInfo actual;
        final List<MethodInfo> bridges = new ArrayList<>();
    
        MethodAndBridges(MethodSignature sig) {}
    
        void set(MethodInfo mi) {
            if(actual != null) throw new IllegalStateException();
            actual = mi;
        }
    
        void addBridge(MethodInfo mi) {
            bridges.add(mi);
        }
    }
    

    We can gather the information in a form ready for checking override relations with the ASM library as follows:

    class MethodCollector extends ClassVisitor {
        static Map<MethodSignature, MethodAndBridges> getMethods(ClassReader cr) {
            MethodCollector mc = new MethodCollector();
            cr.accept(mc, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
            return mc.found;
        }
    
        final Map<MethodSignature, MethodAndBridges> found = new HashMap<>();
        String owner, superClass;
        List<String> interfaces;
    
        protected MethodCollector() {
            super(Opcodes.ASM9);
        }
    
        @Override
        public void visit(int version, int acc,
                String name, String sig, String superName, String[] ifNames) {
            owner = name;
            superClass = superName;
            this.interfaces = ifNames == null? List.of(): List.of(ifNames);
        }
    
        @Override
        public MethodVisitor visitMethod(
            int acc, String name, String desc, String sig, String[] exceptions) {
    
            MethodInfo mi = new MethodInfo(acc, owner, name, desc);
            if((acc & Opcodes.ACC_BRIDGE) == 0) {
                found.computeIfAbsent(mi.signature(), MethodAndBridges::new).set(mi);
                return null;
            }
            return new MethodVisitor(Opcodes.ASM9) {
                @Override public void visitMethodInsn(
                        int op, String owner, String name, String tDesc, boolean i) {
                    found.computeIfAbsent(new MethodSignature(name, tDesc),
                        MethodAndBridges::new).addBridge(mi);
                }
            };
        }
    }
    

    To demonstrate how this work, let’s enhance your example, to address more cases

    interface SupplierOfSerializable {
        Serializable get();
    }
    
    interface ISetting<T extends CharSequence> extends Supplier<T>, Consumer<T> {
        T get();
        @Override void accept(T t);
        Number method(int i);
        static void method(Object o) {}
        private void method(Number n) {}
    }
    
    class Setting implements ISetting<String>, SupplierOfSerializable {
        public String get() {
            return "";
        }
        @Override
        public void accept(String t) {}
        public Integer method(int i) {
            return i;
        }
        static void method(Object o) {}
        void method(Number n) {}
    }
    

    and check the override relations (only considering the direct interfaces, without recursion)

    public class CheckOverride {
        public static void main(String[] args) throws IOException {
            MethodCollector mc = new MethodCollector();
            new ClassReader(Setting.class.getName())
                .accept(mc, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
    
            Map<MethodSignature, MethodAndBridges> implMethods = mc.found;
            Map<MethodInfo, Set<MethodInfo>> overrides = new HashMap<>();
    
            for(String ifType: mc.interfaces) {
                Map<MethodSignature, MethodAndBridges> ifMethods
                    = MethodCollector.getMethods(new ClassReader(ifType));
    
                System.out.println("interface " + ifType.replace('/', '.'));
                printMethods(ifMethods);
                System.out.println();
                ifMethods.values().removeIf(CheckOverride::nonOverridable);
    
                implMethods.forEach((sig, method) -> {
                    if(nonOverridable(method)) {
                        overrides.putIfAbsent(method.actual, Set.of());
                        return;
                    }
                    var overridden = ifMethods.get(sig);
                    if(overridden == null && method.bridges.isEmpty()) {
                        overrides.putIfAbsent(method.actual, Set.of());
                        return;
                    }
                    Set<MethodInfo> set = overrides.compute(method.actual,
                        (k, s) -> s == null || s.isEmpty()? new HashSet<>(): s);
                    if(overridden != null) set.add(overridden.actual);
                    for(var mi: method.bridges) {
                        overridden = ifMethods.get(mi.signature());
                        if(overridden != null) set.add(overridden.actual);
                    }
                });
            }
    
            System.out.println("class " + mc.owner.replace('/', '.'));
            printMethods(implMethods);
            System.out.println();
    
            System.out.println("Final result");
            System.out.println("class " + mc.owner.replace('/', '.'));
            overrides.forEach((m,overridden) -> {
                System.out.println("  " + toDeclaration(m, false));
                if(!overridden.isEmpty()) {
                    System.out.println("    overrides");
                    overridden.forEach(o ->
                        System.out.println("      " + toDeclaration(o, true)));
                }
            });
        }
    
        static boolean nonOverridable(MethodAndBridges m) {
            return (m.actual.access() & (Opcodes.ACC_PRIVATE|Opcodes.ACC_STATIC)) != 0
                 || m.actual.name().startsWith("<");
        }
    
        static void printMethods(Map<MethodSignature, MethodAndBridges> methods) {
            methods.forEach((sig, methodAndBridges) -> {
                System.out.println("  "+toDeclaration(methodAndBridges.actual,false));
                if(!methodAndBridges.bridges.isEmpty()) {
                    System.out.println("    bridges");
                    for(MethodInfo mi: methodAndBridges.bridges) {
                        System.out.println("      " + toDeclaration(mi, false));
                    }
                };
            });
        }
    
        private static String toDeclaration(MethodInfo mi, boolean withType) {
            StringBuilder sb = new StringBuilder();
            sb.append(Modifier.toString(mi.access() & Modifier.methodModifiers()));
            if(sb.length() > 0) sb.append(' ');
            String clName = mi.owner();
            var mt = MethodTypeDesc.ofDescriptor(mi.desc());
            if(mi.name().equals("<init>"))
                sb.append(clName, clName.lastIndexOf('/') + 1, clName.length());
            else {
                sb.append(mt.returnType().displayName()).append(' ');
                if(withType) sb.append(clName.replace('/', '.')).append('.');
                sb.append(mi.name());
            }
            if(mt.parameterCount() == 0) sb.append("()");
            else {
                String sep = "(";
                for(ClassDesc cd: mt.parameterList()) {
                    sb.append(sep).append(cd.displayName());
                    sep = ", ";
                }
                sb.append(')');
            }
            return sb.toString();
        }
    }
    
    interface ISetting
      public static void method(Object)
      public abstract void accept(CharSequence)
        bridges
          public void accept(Object)
      public abstract Number method(int)
      private void method(Number)
      public abstract CharSequence get()
        bridges
          public Object get()
    
    interface SupplierOfSerializable
      public abstract Serializable get()
    
    class Setting
      Setting()
      public Integer method(int)
        bridges
          public Number method(int)
      public void accept(String)
        bridges
          public void accept(Object)
          public void accept(CharSequence)
      static void method(Object)
      public String get()
        bridges
          public Object get()
          public CharSequence get()
          public Serializable get()
      void method(Number)
    
    Final result
    class Setting
      public String get()
        overrides
          public abstract Serializable SupplierOfSerializable.get()
          public abstract CharSequence ISetting.get()
      Setting()
      public Integer method(int)
        overrides
          public abstract Number ISetting.method(int)
      public void accept(String)
        overrides
          public abstract void ISetting.accept(CharSequence)
      void method(Number)
      static void method(Object)
    

    The code uses newer Java features, like var, record, and the constant API, but I think, the result is straight-forward enough for converting it to older Java versions, if really required.