Java:找到所有直接&间接引用带注释的方法

时间:2017-12-12 18:31:55

标签: java java-bytecode-asm annotation-processing

假设我在api.jar中定义了以下API:

public class API {
    // @RequiredActionsPermitted({"1","3"})
    public void higherApi1() { highApi1(); highApi3(); }

    public void highApi1() { lowApi1(); }
    public void highApi2() { lowApi1(); lowApi2(); }
    public void highApi3() { lowApi3(); }

    @RequiredActionsPermitted({"1"})    public void lowApi1() {}
    @RequiredActionsPermitted({"2"})    public void lowApi2() {}
    @RequiredActionsPermitted({"3"})    public void lowApi3() {}

    public void zMethod() { lowApi1(); lowApi2(); lowApi3(); }
}

在另一个client.jar中有一个这样的主方法(上面的api.jar包含在二进制依赖项中):

public static void main(String[] args) {
    API api = new API();
    api.higherApi1();
}

假设这个main()方法调用了highApi1(),后者又间接调用了lowApi1()和lowApi3(),我想生成一些这样的XML输出:

<requiredActions>
   <action>1</action>
   <action>3</action>
</requiredActions>

动作2不应该是输出的一部分,因为main()方法不直接或间接地调用lowApi2()。

当然,我可以使用示例中注释掉的注释轻松注释higherApi3(),但是如果你有很多间接方法调用,这将很难维护。

编辑:我现在有一个基于ASM的解决方案,主要用于:

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

// TODO Add documentation
// TODO clean up this class
public class TokenSpecGenerator {
    public static void main(String[] jars) throws IOException {
        Map<String, Set<String>> methodsToAnnotationValuesMap = new HashMap<>();
        if (findAnnotatedMethods(jars, methodsToAnnotationValuesMap)) {
            while (findIndirectMethodInvocations(jars, methodsToAnnotationValuesMap)) {
            }
        }
        // TODO Should we also report any API method invocations outside of
        // methods?
        // (i.e. field initializers, static blocks, ...)
        for (Map.Entry<String, Set<String>> entry : methodsToAnnotationValuesMap.entrySet()) {
            if (!entry.getKey().startsWith("com/something/myapi")) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
    }

    protected static boolean findAnnotatedMethods(String[] jars, Map<String, Set<String>> methodsToAnnotationValuesMap) {
        System.out.println("Scanning for methods annotated with @RequiredActionsPermitted");
        FindAnnotatedMethods classVisitor = new FindAnnotatedMethods(methodsToAnnotationValuesMap);
        visitClasses(jars, classVisitor);
        System.out.println("Methods and required actions permitted found until now:");
        System.out.println(methodsToAnnotationValuesMap);
        return classVisitor.hasFoundNew();
    }

    protected static boolean findIndirectMethodInvocations(String[] jars, Map<String, Set<String>> methodsToAnnotationValuesMap) {
        System.out.println("Next round of scanning for methods that indirectly call methods annotated with @RequiredActionsPermitted");
        FindMethodInvocations classVisitor = new FindMethodInvocations(methodsToAnnotationValuesMap);
        visitClasses(jars, classVisitor);
        System.out.println("Methods and required actions permitted found until now:");
        System.out.println(methodsToAnnotationValuesMap);
        return classVisitor.hasFoundNew();
    }

    protected static void visitClasses(String[] jars, ClassVisitor classVisitor) {
        for (String jar : jars) {
            JarFile jarFile = null;
            try {
                try {
                    jarFile = new JarFile(jar);
                    Enumeration<JarEntry> entries = jarFile.entries();

                    while (entries.hasMoreElements()) {
                        JarEntry entry = entries.nextElement();

                        if (entry.getName().endsWith(".class")) {
                            try {
                                InputStream stream = null;
                                try {
                                    stream = new BufferedInputStream(jarFile.getInputStream(entry), 1024);
                                    new ClassReader(stream).accept(classVisitor, 0);
                                } finally {
                                    stream.close();
                                }
                            } catch ( IOException e ) { e.printStackTrace(); }
                        }
                    }
                } finally {
                    jarFile.close();
                }
            } catch ( IOException e ) { e.printStackTrace(); }
        }
    }

    static abstract class AbstractClassVisitor extends ClassVisitor {
        private final Map<String, Set<String>> methodsToAnnotationValuesMap;
        private boolean foundNew = false;
        private String className;

        public AbstractClassVisitor(Map<String, Set<String>> methodsToAnnotationValuesMap) {
            super(Opcodes.ASM5);
            this.methodsToAnnotationValuesMap = methodsToAnnotationValuesMap;
        }

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            this.className = name;
            super.visit(version, access, name, signature, superName, interfaces);
        }

        protected void addValues(String method, Set<String> values) {
            Set<String> existingValues = methodsToAnnotationValuesMap.get(method);
            if (existingValues == null) {
                existingValues = new HashSet<String>();
                methodsToAnnotationValuesMap.put(method, existingValues);
            }
            foundNew |= existingValues.addAll(values);
        }

        public boolean hasFoundNew() {
            return foundNew;
        }

        public String getClassName() {
            return className;
        }

        public Map<String, Set<String>> getMethodsToAnnotationValuesMap() {
            return methodsToAnnotationValuesMap;
        }
    }

    static abstract class AbstractMethodVisitor extends MethodVisitor {
        private final String className;
        private final String methodName;

        public AbstractMethodVisitor(String className, String methodName) {
            super(Opcodes.ASM5);
            this.className = className;
            this.methodName = methodName;
        }

        // TODO Add method parameter types
        public String getMethodDescription() {
            return className+"."+methodName;
        }
    }

    static class FindAnnotatedMethods extends AbstractClassVisitor {
        public FindAnnotatedMethods(Map<String, Set<String>> methodsToAnnotationValuesMap) {
            super(methodsToAnnotationValuesMap);
        }

        @Override
        public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
            return new AbstractMethodVisitor(getClassName(), name) {
                private Set<String> values = new HashSet<>();
                @Override
                public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                    if ("Lcom/something/myapi/annotation/RequiredActionsPermitted;".equals(desc)) {
                        return new AnnotationVisitor(Opcodes.ASM5) {
                            @Override
                            public AnnotationVisitor visitArray(String name) {
                                if ( !"value".equals(name) ) {
                                    return null;
                                } else {
                                    return new AnnotationVisitor(Opcodes.ASM5) {
                                        @Override
                                        public void visit(String name, Object value) {
                                            values.add((String)value);
                                        }
                                    };
                                }
                            }
                        };
                    }
                    return null;
                }

                @Override
                public void visitEnd() {
                    if ( !values.isEmpty() ) {
                        addValues(getMethodDescription(), values);
                    }
                }
            };
        }
    }

    static class FindMethodInvocations extends AbstractClassVisitor {
        public FindMethodInvocations(Map<String, Set<String>> methodsToAnnotationValuesMap) {
            super(methodsToAnnotationValuesMap);
        }

        @Override
        public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
            return new AbstractMethodVisitor(getClassName(), name) {
                private Set<String> values = new HashSet<>();
                @Override
                public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
                    String method = owner+"."+name;
                    if ( getMethodsToAnnotationValuesMap().containsKey(method) ) {
                        values.addAll(getMethodsToAnnotationValuesMap().get(method));
                    }
                }

                @Override
                public void visitEnd() {
                    if ( !values.isEmpty() ) {
                        addValues(getMethodDescription(), values);
                    }
                }
            };
        }
    }

}

我仍然有以下问题/问题:

  • 目前,方法仅在类和方法名称上匹配。要正确处理重载方法,匹配还应包括方法参数类型。知道如何在ASM访客中获取此信息吗?

  • 将visitMethodInsn()中的方法调用与之前由methodsToAnnotationValuesMap中的visitMethod()存储的方法信息进行匹配的更好方法是什么?

  • 除了清理和添加JavaDoc之外,还有其他改进此实现的建议吗?

谢谢, 路德

  

以前的方法

     

仅供参考,之前我尝试使用Annotation Processors实现此功能。下面是我的注释处理器实现,它找到一些间接方法调用但不是全部(取决于Javac编译方法和类的顺序)。我现在已经切换到上面列出的基于ASM的方法,但是将其留在此处作为注释处理的示例。

  @SupportedAnnotationTypes("com.something.annotation.RequiredActionsPermitted")
  @SupportedSourceVersion(SourceVersion.RELEASE_7)
  public class RequiredActionsPermittedProcessor extends AbstractProcessor implements TaskListener {
      private final Map<String, List<String>  >    callers = new HashMap<>    ();
      Trees trees;

      @Override
      public synchronized void init(ProcessingEnvironment processingEnv) {
          super.init(processingEnv);
          trees = Trees.instance(processingEnv);
          JavacTask.instance(processingEnv).setTaskListener(this);
      }

      @Override
      public boolean process(Set<? extends TypeElement>    annotations, RoundEnvironment roundEnv) {
          return true;
      }



      @Override
      public void finished(final TaskEvent taskEvt) {
          if (taskEvt.getKind() == TaskEvent.Kind.ANALYZE) {
              //System.out.println("!!!! TEST4 !!!!");
              taskEvt.getCompilationUnit().accept(new TreeScanner<Void, Void> () {
                  private MethodTree parentMethod = null;

                  @Override
                  public Void visitMethod(MethodTree methodTree, Void arg1) {
                      this.parentMethod = methodTree;

                      return super.visitMethod(methodTree, arg1);
                  }

                  @Override
                  public Void visitMethodInvocation(MethodInvocationTree methodInv, Void v) {
                      //System.out.println("!!!! TEST5 !!!!: "+methodInv);
                      JCTree jcTree = (JCTree) methodInv.getMethodSelect();
                      Element method = TreeInfo.symbol(jcTree);
                      RequiredActionsPermitted RequiredActionsPermitted = method.getAnnotation(RequiredActionsPermitted.class);
                      //System.out.println("!!!! TEST0 !!!!: "+method.getSimpleName().toString());
                      String methodName = method.getSimpleName().toString();
                      if ( parentMethod != null ) {
                          String parentName = parentMethod.getName().toString();
                          List<String>     values = null;
                          if (RequiredActionsPermitted != null ) {
                              values = Arrays.asList(RequiredActionsPermitted.value());
                          } else if ( callers.containsKey(methodName) ) {
                              values = callers.get(methodName);
                          }
                          if ( values != null ) {
                              List<String>     currentValues = callers.get(parentName);
                              if ( currentValues == null ) {
                                  currentValues = new ArrayList<> ();
                                  callers.put(parentName, currentValues);
                              }
                              currentValues.addAll(values);
                              System.out.println("!!!! TEST4 !!!!: Parent "+parentName);
                              System.out.println("!!!! TEST5 !!!!: Calls "+jcTree+" - "+String.join(",", values));
                          }
                      }
                      return super.visitMethodInvocation(methodInv, v);
                  }
              }, null);
          }
      }

      @Override
      public void started(TaskEvent taskEvt) {}
  }

0 个答案:

没有答案