Java – 从类加载器中查找返回特定类型的所有方法

huangapple go评论76阅读模式
英文:

Java - Find all the methods returning a specific type from classloader

问题

I am looking for a way to find all the methods from all the classes from my classloader which returns some type. For example List.

我正在寻找一种方法,可以从我的类加载器中找到所有返回某种类型的所有类的方法,例如List

I tried to use this library - https://github.com/ronmamo/reflections

我尝试使用这个库 - https://github.com/ronmamo/reflections

Reflections reflections = new Reflections("com", new MethodParameterScanner());

Reflections reflections = new Reflections("com", new MethodParameterScanner());

Set methodsReturn = reflections.getMethodsReturn(List.class);
methodsReturn
.forEach(c -> System.out.println(c.getDeclaringClass() + "-" + c.getName()));

Set methodsReturn = reflections.getMethodsReturn(List.class);
methodsReturn
.forEach(c -> System.out.println(c.getDeclaringClass() + "-" + c.getName()));

But failed with this exception.

但是出现了以下异常。

org.reflections.ReflectionsException: could not get type for name com.intellij.rt.execution.CommandLineWrapper$AppData.access
at org.reflections.ReflectionUtils.forName(ReflectionUtils.java:312)
at org.reflections.util.Utils.getMemberFromDescriptor(Utils.java:67)
at org.reflections.util.Utils.getMethodsFromDescriptors(Utils.java:88)

org.reflections.ReflectionsException: 无法获取名称为com.intellij.rt.execution.CommandLineWrapper$AppData.access的类型
at org.reflections.ReflectionUtils.forName(ReflectionUtils.java:312)
at org.reflections.util.Utils.getMemberFromDescriptor(Utils.java:67)
at org.reflections.util.Utils.getMethodsFromDescriptors(Utils.java:88)

Is there any other way to do this?

是否有其他方法可以实现这个目标?

英文:

I am looking for a way to find all the methods from all the classes from my classloader which returns some type. For example List.

I tried to use this library - https://github.com/ronmamo/reflections

Reflections reflections = new Reflections("com",new MethodParameterScanner());

        Set<Method> methodsReturn = reflections.getMethodsReturn(List.class);
        methodsReturn
                .forEach(c -> System.out.println(c.getDeclaringClass() + "-" + c.getName()));

But failed with this exception.

org.reflections.ReflectionsException: could not get type for name com.intellij.rt.execution.CommandLineWrapper$AppData.access
	at org.reflections.ReflectionUtils.forName(ReflectionUtils.java:312)
	at org.reflections.util.Utils.getMemberFromDescriptor(Utils.java:67)
	at org.reflections.util.Utils.getMethodsFromDescriptors(Utils.java:88)

Is there any other way to do this?

答案1

得分: 0

你想要的并不可行。反射(reflections)是一个有漏洞的抽象;一个没有保证的黑客。

问题在于ClassLoader是一个抽象,基本上只有以下一个操作可用:

  • 给定一个资源名称,返回类加载器可以找到的该资源的所有版本;通常,几乎总是0或1个项目。一个“资源”是一个URL;可以由此加载程序保证转换为InputStream的URL。

就是这样

注意到这里__没有方法来请求所有资源的列表__,因此,任何尝试从类加载器中获取“获取具有属性X的所有资源”的操作都__会失败__。这就像试图从石头里汲水一样。你不能得到不存在的东西。

反射黑客的做法是检查它是哪种类型的类加载器。如果它识别出类加载器作为从具有列出所有操作的地方获取资源的东西(例如JAR文件或文件系统目录),它将获取底层位置并在这些资源上执行直接的“列出”操作;但是,如果涉及的类加载器不是反射识别的类型,或者不是由具有列出操作的存储支持的类型,那么反射要么会默默地忽略它,要么会崩溃。

这导致了不可避免的结论:你不想做你说你想做的事情;Java不支持这样做。

即使你接受了你的程序只能使用基本的JAR/目录类加载器才能正常运行的事实,你所做的事情实际上是将Java转化为结构化类型的语言。Java不是结构化类型的语言;它是围绕名义类型设计的。如果你遵循这个设计,Java将工作得更好,因为其他编写Java代码的人会假定不会发生这种情况。

例如,如果你这样说:“我正在寻找各种各样的相机,所以我将扫描整个类路径,查找任何具有void shoot(Person p)签名方法的类,并在自拍选项列表中提供这些选项”,那么当某种方式在你的类路径中出现Gun类时,你将会度过一个__非常糟糕的时光__。

在Java中,两个方法__基本上__没有任何关系,即使它们具有相同的名称 - 一个方法由整个签名定义,包括它所在的类型。这个事实贯穿于Java的语言设计中。

那么,在Java中__如何__做这样的事情呢?

你可能想要的是SPI。

  • SPI与所有类加载器__100%兼容__,现在和将来都是如此。
  • Java本身在许多地方使用SPI。
  • SPI足够常见,许多IDE和构建系统内置了插件和工具。
  • 它__完全__依赖于名义类型。

下面是SPI的工作原理:

  • 定义一个接口,或者如果必要,一个(抽象)类。
  • “插件”将执行两件事情:[1] 创建一个可以实例化的类(public,非抽象,具有公共无参数构造函数),[2] 实现/扩展我们在第一步中创建的类型,[3] 在名为META-INF/services/com.foo.full.package.name.ThatInterfaceFromStep1的文件中列出这个类的完全限定名称。

然后,主工具执行以下操作:

  • 向类加载器请求匹配名称为META-INF/services/com.foo.full.package.name.ThatInterfaceFromStep1的__所有__资源,然后读取所有这些资源的所有行。
  • 然后,它加载通过这种方式找到的每个名称,并将它们提供为实现。你可以将所有这些都视为ThatInterfaceFromStep1的实例,因为它们是。不可能发生意外的铅中毒。

你可以使用注解处理器,这样你只需要编写,比如:

@Provides(ImageSaver.class)
public class Jpg2000Saver implements ImageSaver {
    @Override public String description() {
        return "Joint Photographic Experts Group 2000 (JP2)";
    }

    @Override public void write(Image image, OutputStream target) throws IOException {
        // .... 在这里实现
    }
}

相关的注解处理器将负责为你创建那个META-INF文件。或者,你可以自己创建该文件;例如,如果你使用Maven,将其放在src/resources中。要“加载”SPI文件,Java本身已经内置了这个功能:ServiceLoader

英文:

What you want is not possible. reflections is a leaky abstraction; a hack with no guarantees.

The problem is that ClassLoader is an abstraction which fundamentally only has the following singular operation available:

  • Given a resource name, return all versions of this resource that the classloader can find; usually, almost always, this will be 0 or 1 items. A 'resource' is a URL; one that can guaranteed be turned into an InputStream by this loader.

That is it.

Note how there is no method to ask for a list of all resources, and as a consequence, any attempt to do 'get me all resources that have property X' from a classloader is broken. It's like trying to draw water from a stone. You can't get what aint there.

What the reflections hack does is inspect what kind of classloader it is. If it recognizes the classloader as a thing that gets resources from a place that does have an operation to list all (such as a jar file or filesystem directory), it'll obtain the underlying locations and do direct 'list' operations on those resources, but, if the classloader involved is not one that reflections recognizes, or is one that is not backed by a store that has a list operation, then reflections will either silently ignore it, or crash.

This leads to the inevitable conclusion: You don't want to do what you said you want to do; it is not supported by java.

Even if you accept that your program will fail unless only basic jar/dir based classloaders are used, what you're doing is turning java into a structurally typed language. Java isn't structurally typed; it is designed around nominal typing. Not only does java just work better if you follow suit, you're doing things which other people writing java code assume never happen.

For example, if you go: "I am looking for all sorts of cameras, so I will just scan the entire classpath for any class that has a method with signature void shoot(Person p) and offer this in a list of options for making selfies", then you're going to have an extremely bad time when somehow the class Gun ends up in your classpath.

In java, two methods fundamentally do not relate, whatsoever, even if they have the same name - a method is defined by the ENTIRE signature, including the type it is in. This fact is all over java's language design.

So, how do you do things like this in java?

What you probably want is SPI.

  • SPI is 100% compatible with all classloaders, now and forever.
  • Java itself uses SPI in many places.
  • SPI is common enough that there are plugins and tools built into many IDEs and build systems.
  • It works solely on nominal typing.

Here's how SPI works:

  • Define an interface or if you must, an (abstract) class.
  • "Plugins" will be doing two things:
  • [1] make a class that is instantiable (public, not abstract, has a public no-args constructor), [2] implements/extends that type we made in the first step, and [3] list the fully qualified name of this class in a file named META-INF/services/com.foo.full.package.name.ThatInterfaceFromStep1.

Then, the main tool does the following:

  • Asks the classloader for all resources matching the name META-INF/services/com.foo.full.package.name.ThatInterfaceFromStep1, and then reads all lines from all of these.
  • It then classloads each name it finds this way, and offers them as implementations. You can treat all of these as instances of ThatInterfaceFromStep1, because they are. No accidental lead poisoning possible.

You can use an annotation processor so that all you have to do is write, say:

@Provides(ImageSaver.class)
public class Jpg2000Saver implements ImageSaver {
    @Override public String description() {
        return "Joint Photographic Experts Group 2000 (JP2)";
    }

    @Override public void write(Image image, OutputStream target) throws IOException {
        // .... implement here
    }
}

and the associated annotation processor will take care of making that META-INF file for you. Alternatively, make that file yourself; e.g. if you use maven, put that in src/resources. To 'load' SPI files, that's built into java itself: ServiceLoader.

huangapple
  • 本文由 发表于 2020年8月10日 00:55:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/63329033.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定