kotlin中使用java反射包——Can't resolve member named default for class异常处理

1,282 阅读2分钟

背景

java项目中经常通过java反射机制获取注解标记的类方法,但是当由于kotlin反射性能问题,尝试在kotlin中尝试使用java反射包获取注解标记的类方法时,却抛出了异常

Caused by: org.reflections.ReflectionsException: Can't resolve member named default for class com.xx.xx
	at org.reflections.util.Utils.getMemberFromDescriptor(Utils.java:94) ~[reflections-0.9.11.jar:?]
	at org.reflections.util.Utils.getMethodsFromDescriptors(Utils.java:101) ~[reflections-0.9.11.jar:?]
	at org.reflections.Reflections.getMethodsAnnotatedWith(Reflections.java:482) ~[reflections-0.9.11.jar:?]

排查

既然有堆栈打印,那直接追踪代码
=> org.reflections.util.Utils: 94

public static Member getMemberFromDescriptor(String descriptor, ClassLoader... classLoaders) throws ReflectionsException {
    int p0 = descriptor.lastIndexOf('(');
    String memberKey = p0 != -1 ? descriptor.substring(0, p0) : descriptor;
    String methodParameters = p0 != -1 ? descriptor.substring(p0 + 1, descriptor.lastIndexOf(')')) : "";

    int p1 = Math.max(memberKey.lastIndexOf('.'), memberKey.lastIndexOf("$"));
    String className = memberKey.substring(memberKey.lastIndexOf(' ') + 1, p1);
    String memberName = memberKey.substring(p1 + 1);

    Class<?>[] parameterTypes = null;
    if (!isEmpty(methodParameters)) {
        String[] parameterNames = methodParameters.split(",");
        List<Class<?>> result = new ArrayList<Class<?>>(parameterNames.length);
        for (String name : parameterNames) {
            result.add(forName(name.trim(), classLoaders));
        }
        parameterTypes = result.toArray(new Class<?>[result.size()]);
    }

    Class<?> aClass = forName(className, classLoaders);
    while (aClass != null) {
        try {
            if (!descriptor.contains("(")) {
                return aClass.isInterface() ? aClass.getField(memberName) : aClass.getDeclaredField(memberName);
            } else if (isConstructor(descriptor)) {
                return aClass.isInterface() ? aClass.getConstructor(parameterTypes) : aClass.getDeclaredConstructor(parameterTypes);
            } else {
                return aClass.isInterface() ? aClass.getMethod(memberName, parameterTypes) : aClass.getDeclaredMethod(memberName, parameterTypes);
            }
        } catch (Exception e) {
            aClass = aClass.getSuperclass();
        }
    }
    throw new ReflectionsException("Can't resolve member named " + memberName + " for class " + className);
}

上述代码中看出,最终有2个条件会导致异常抛出

  1. forName获取的aClass为空
  2. forName获取的aClass在获取反射信息时报错,而且超类为空

因此,在while语句打上条件断点memberName.equals("default"),debug

从图中可以看出,是forName获取的aClass为空
同时也看到classNamecom.xx.xx.XXController.playCapsule,但是playCapsule是个方法,而不是类,forName自然是为空的
那现在问题就在于,为什么这个方法名会判断为类名呢?
回看一下代码,

int p0 = descriptor.lastIndexOf('(');
String memberKey = p0 != -1 ? descriptor.substring(0, p0) : descriptor;
......
int p1 = Math.max(memberKey.lastIndexOf('.'), memberKey.lastIndexOf("$"));
String className = memberKey.substring(memberKey.lastIndexOf(' ') + 1, p1);
......

发现className是通过descriptor截取(以左,最右的.和最右的$两者中最右的以左的字符串,结果

descriptor = com.xx.xx.XXController.playCapsule$default(......)
className = com.xx.xx.XXController.playCapsule

那这个descriptor又是如何产生的?通过debug堆栈向上跟进代码

=> org.reflections.util.Utils: 101

public static Set<Method> getMethodsFromDescriptors(Iterable<String> annotatedWith, ClassLoader... classLoaders) {
    Set<Method> result = Sets.newHashSet();
    for (String annotated : annotatedWith) {
        if (!isConstructor(annotated)) {
            Method member = (Method) getMemberFromDescriptor(annotated, classLoaders);
            if (member != null) result.add(member);
        }
    }
    return result;
}

图中看到,descriptor是被注解方法annotatedWith集合中的一个 同时也看到这里标记了两个方法

com.xx.xx.XXController.playCapsule$default(......)
com.xx.xx.XXController.playCapsule(......)

这里再结合原方法代码

@OptionalAuthAPI
@PostMapping("/xxx")
fun playCapsule(
        @OptionalAuthRes authRes: OptionalAuthResDTO,
        @PathVariable xxxx: Int,
        @RequestBody(required = false) capsulePlayOrigin: CapsulePlayOrigin? = null,
        request: HttpServletRequest
): DataMap {......}

看出原来是kotlin生成的默认参数方法

解决

那简单的解决方法自然就是不使用kotlin默认参数即可
但是这样约定限制总可能会有疏忽,有没有更自动化的方式呢?那我们就得看看annotatedWith是怎么来的了
dubug堆栈继续跟进

=> org.reflections.Reflections: 482

/**
 * get all methods annotated with a given annotation
 * <p/>depends on MethodAnnotationsScanner configured
 */
public Set<Method> getMethodsAnnotatedWith(final Class<? extends Annotation> annotation) {
    Iterable<String> methods = store.get(index(MethodAnnotationsScanner.class), annotation.getName());
    return getMethodsFromDescriptors(methods, loaders());
}

可以看出,annotatedWith是从store中根据 注解类型名

private static String index(Class<? extends Scanner> scannerClass) { return scannerClass.getSimpleName(); }

和 注解名

annotation.getName()

获取的
store类似于映射表,根据 注解类型名 和 注解名,存储注解标记的元素
那如果需要对 默认参数方法 进行过滤调整,就必须在store加入对象时过滤
跟踪store的使用,发现
=> org.reflections.Reflections: 112

/**
 * constructs a Reflections instance and scan according to given {@link org.reflections.Configuration}
 * <p>it is preferred to use {@link org.reflections.util.ConfigurationBuilder}
 */
public Reflections(final Configuration configuration) {
    this.configuration = configuration;
    store = new Store(configuration);

    if (configuration.getScanners() != null && !configuration.getScanners().isEmpty()) {
        //inject to scanners
        for (Scanner scanner : configuration.getScanners()) {
            scanner.setConfiguration(configuration);
            scanner.setStore(store.getOrCreate(scanner.getClass().getSimpleName()));
        }

        scan();

        if (configuration.shouldExpandSuperTypes()) {
            expandSuperTypes();
        }
    }
}

store会在Reflections初始化时被注入scanner中,然后进行扫描录入被注解元素
跟进
=> org.reflections.Reflections: 177

protected void scan() {
    if (configuration.getUrls() == null || configuration.getUrls().isEmpty()) {
        if (log != null) log.warn("given scan urls are empty. set urls in the configuration");
        return;
    }

    if (log != null && log.isDebugEnabled()) {
        log.debug("going to scan these urls:\n" + Joiner.on("\n").join(configuration.getUrls()));
    }

    long time = System.currentTimeMillis();
    int scannedUrls = 0;
    ExecutorService executorService = configuration.getExecutorService();
    List<Future<?>> futures = Lists.newArrayList();

    for (final URL url : configuration.getUrls()) {
        try {
            if (executorService != null) {
                futures.add(executorService.submit(new Runnable() {
                    public void run() {
                        if (log != null && log.isDebugEnabled()) log.debug("[" + Thread.currentThread().toString() + "] scanning " + url);
                        scan(url);
                    }
                }));
            } else {
                scan(url);
            }
            scannedUrls++;
        } catch (ReflectionsException e) {
            if (log != null && log.isWarnEnabled()) log.warn("could not create Vfs.Dir from url. ignoring the exception and continuing", e);
        }
    }

    //todo use CompletionService
    if (executorService != null) {
        for (Future future : futures) {
            try { future.get(); } catch (Exception e) { throw new RuntimeException(e); }
        }
    }

    time = System.currentTimeMillis() - time;

    //gracefully shutdown the parallel scanner executor service.
    if (executorService != null) {
        executorService.shutdown();
    }

    if (log != null) {
        int keys = 0;
        int values = 0;
        for (String index : store.keySet()) {
            keys += store.get(index).keySet().size();
            values += store.get(index).size();
        }

        log.info(format("Reflections took %d ms to scan %d urls, producing %d keys and %d values %s",
                time, scannedUrls, keys, values,
                executorService != null && executorService instanceof ThreadPoolExecutor ?
                        format("[using %d cores]", ((ThreadPoolExecutor) executorService).getMaximumPoolSize()) : ""));
    }
}

其中重要部分在

for (final URL url : configuration.getUrls()) {
    try {
        if (executorService != null) {
            futures.add(executorService.submit(new Runnable() {
                public void run() {
                    if (log != null && log.isDebugEnabled()) log.debug("[" + Thread.currentThread().toString() + "] scanning " + url);
                    scan(url);
                }
            }));
        } else {
            scan(url);
        }
        scannedUrls++;
    } catch (ReflectionsException e) {
        if (log != null && log.isWarnEnabled()) log.warn("could not create Vfs.Dir from url. ignoring the exception and continuing", e);
    }
}

这里会根据配置选择同步或异步扫描,以及扫描路径,再调用下个scan
=> org.reflections.Reflections: 239

protected void scan(URL url) {
    Vfs.Dir dir = Vfs.fromURL(url);

    try {
        for (final Vfs.File file : dir.getFiles()) {
            // scan if inputs filter accepts file relative path or fqn
            Predicate<String> inputsFilter = configuration.getInputsFilter();
            String path = file.getRelativePath();
            String fqn = path.replace('/', '.');
            if (inputsFilter == null || inputsFilter.apply(path) || inputsFilter.apply(fqn)) {
                Object classObject = null;
                for (Scanner scanner : configuration.getScanners()) {
                    try {
                        if (scanner.acceptsInput(path) || scanner.acceptResult(fqn)) {
                            classObject = scanner.scan(file, classObject);
                        }
                    } catch (Exception e) {
                        if (log != null && log.isDebugEnabled())
                            log.debug("could not scan file " + file.getRelativePath() + " in url " + url.toExternalForm() + " with scanner " + scanner.getClass().getSimpleName(), e);
                    }
                }
            }
        }
    } finally {
        dir.close();
    }
}

这里就看到程序通过scanner.scan(file, classObject)调用扫描器扫描指定路径上的注解
这里调用的是org.reflections.scanners.Scanner接口方法

Object scan(Vfs.File file, @Nullable Object classObject);

我在程序中用到的是org.reflections.scanners.MethodAnnotationsScanner,其继承了org.reflections.scanners.AbstractScannerAbstractScanner实现了接口方法scan
=> org.reflections.scanners.AbstractScanner: 27

public Object scan(Vfs.File file, Object classObject) {
    if (classObject == null) {
        try {
            classObject = configuration.getMetadataAdapter().getOfCreateClassObject(file);
        } catch (Exception e) {
            throw new ReflectionsException("could not create class object from file " + file.getRelativePath(), e);
        }
    }
    scan(classObject);
    return classObject;
}

public abstract void scan(Object cls);

然后,MethodAnnotationsScanner实现了虚拟方法scan
=> org.reflections.scanners.MethodAnnotationsScanner: 8

public void scan(final Object cls) {
    for (Object method : getMetadataAdapter().getMethods(cls)) {
        for (String methodAnnotation : (List<String>) getMetadataAdapter().getMethodAnnotationNames(method)) {
            if (acceptResult(methodAnnotation)) {
                getStore().put(methodAnnotation, getMetadataAdapter().getMethodFullKey(cls, method));
            }
        }
    }
}

这里可以看到,扫描器将带有注解的方法扫描通过后,便录入store
虽然Scanner没有提供锚点让我们过滤处理方法名(acceptResult(methodAnnotation)是过滤处理注解),但是我们可以重写scan方法实现,让其过滤处理方法名

class FilterMethodNameMethodAnnotationScanner: MethodAnnotationsScanner() {

    @Suppress("UNCHECKED_CAST")
    override fun scan(cls: Any?) {
        for (method in metadataAdapter.getMethods(cls)) {
            for (methodAnnotation in metadataAdapter.getMethodAnnotationNames(method) as List<String>) {
                if (acceptResult(methodAnnotation)) {
                    val methodFullKey = metadataAdapter.getMethodFullKey(cls, method)
                    /**
                     * 由于kotlin的方法可以指定方法参数默认值,
                     * 导致kotlin会生成含有 $default 字串的默认参数方法,
                     * 但 $ 偏偏又是java反射检测的关键符号之一,
                     * 所以需要过滤掉kotlin生成的默认参数方法
                     */
                     // 这里也可以根据具体需求,定制其他的处理方法
                    if (!methodFullKey.contains("\$default")) 
                        store.put(methodAnnotation, metadataAdapter.getMethodFullKey(cls, method))
                }
            }
        }
    }

}

Reflections没有提供根据Scanner类型获取注解方法的方法,因此还得继承Reflections,新增通过FilterMethodNameMethodAnnotationScanner获取注解方法的方法

class FilterMethodNameReflections(config: Configuration): Reflections(config) {

    fun getMethodsAnnotatedWithForAuth(annotation: Class<out Annotation>): MutableSet<Method> {
        val methods = store.get(index(MethodAnnotationForAuthScanner::class.java), annotation.name)
        return getMethodsFromDescriptors(methods, *loaders())
    }

    private fun index(scannerClass: Class<out Scanner>): String {
        return scannerClass.simpleName
    }

    private fun loaders(): Array<ClassLoader> {
        return configuration.classLoaders?: emptyArray()
    }

}

至此,我们使用FilterMethodNameReflections调用方法,通过FilterMethodNameMethodAnnotationScanner扫描处理注解标记的方法,过滤处理kotlin生成的默认参数方法,即可解决当前问题

结语

上述方法,如果只是单纯过滤,会使kotlin默认参数方法被扫描器忽略,如果只是获取方法名等与kotlin默认参数无关的数据,那可行。但需要获取kotlin默认参数有关数据,则可能得另思特殊处理。