背景
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个条件会导致异常抛出
forName获取的aClass为空forName获取的aClass在获取反射信息时报错,而且超类为空
因此,在while语句打上条件断点memberName.equals("default"),debug

从图中可以看出,是forName获取的aClass为空
同时也看到className是com.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.AbstractScanner,AbstractScanner实现了接口方法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默认参数有关数据,则可能得另思特殊处理。