0

Lets say i have a method in some class in my application's package NetBeans project:

package wuzzle.woozle;

import org.contoso.frobber.grob.Whiztactular;

@Whiztactular
public void testFizBuzz() {
    if (1 != 0) 
       throw new Exception("Whiztactular failed");
}
package frob;

import org.contoso.frobber.grob.Whiztactular;

@Whiztactular
public void testfrobFizBuzz() {
    if (1 != 0) 
       throw new Exception("Whiztactular failed");
}
package grob;

import org.contoso.frobber.grob.Whiztactular;

@Whiztactular
public void testGrobZoom() {
    if (1 != 0) 

       throw new Exception("Whiztactular failed");
}
package contoso.gurundy;

import org.contoso.frobber.grob.Whiztactular;

@Whiztactular
public void testDingbatWoozle() {
    if (1 != 0) 
       throw new Exception("Whiztactular failed");
       throw new Exception("Whiztactular failed");
}

I want to:

  • enumerate all classes/methods
  • find methods tagged with a specified @Annotation
  • construct the class
  • call the (parameterless) method

How can i do this in Java?

In .NET it's easy

Here's how you do it in .NET (in pseudo-Java):

//Find all methods in all classes tagged with @Test annotation, 
//and add them to a list.
List<MethodInfo> whiztactularMethods = new ArrayList<>();

//Enumerate all assemblies in the current application domain
for (Assembly a : AppDomain.currentDomain.getAssemblies()) {
   //Look at each type (i.e. class) in the assembly
   for (Type t : a.getTypes()) {
      //Look at all methods in the class. 
      for (MethodInfo m : t.getMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly)) {
         //If the method has our @Whiztactular annotation defined: add it
         if (m.IsDefined(typeof(org.contoso.frobber.grob.Whiztactular), true)) 
            whiztactularMethods .add(m);
      }
   }
}

And now that we have a List of all methods with the @Whiztactular annotation, it's just a matter of calling them:

//Call every test method found above
for (MethodInfo m : whiztactularMethods) {
   Object o = Activator.CreateInstance(m.DeclaringType); //Construct the test object
   m.Invoke(o, null); //call the parameterless Whiztactular method
}

What is the JRE equivalent of the above?

In Delphi it's easy to

When a Delphi application starts, the initializer of each unit is called:

initialization
   WhiztactularRunner.registerWhiztactularClass(TWuzzleWoozle);

So then i can have all my test code register itself.

But Java doesn't have .java file initialization; nor does it have static constructors.

The Journey

I want JUnit to run tests

JUnit requires tests to be in a special separate project

Use reflection to find the test methods

Reflection requires you to know the name of the packages that all developers have put their tests in

Use Reflections library

Reflections requires you to know the name of the packages that all developers have put their tests in

Create my own Test Annotation, and use reflections to find all methods that are tagged with it

Reflections requires you to know the name of the packages that all developers have put their tests in

Create my own TestCase annotation, and use reflections to find all classes that are tagged with it

Reflections requires you to know the name of the packages that all developers have put their tests in

Create my own TestCase interface, and use reflections to find all classes that implement it

Reflections requires you to know the name of the packages that all developers have put their tests in

Create my own TestCase class, and use reflections to find all classes that extend it

Reflections requires you to know the name of the packages that all developers have put their tests in

Create a static list, and use a static class constructor to register the class with the my TestRunner

Java doesn't have static class constructors

Create a static list, and use the package initializer to register the class with the my TestRunner

Java doesn't have package initializers

Create a static list, and use the events to listen for when a package is loaded, and then register the package with my static list

Java doesn't have package load events

Enumerate all packages

Reflection has no way to enumerate all packages

Ask the class loader that loaded my current class for any other classes it has loaded

Class loader won't know about classes until someone has actually needed them, and you might not even be using the same class loader instance

Enumerate all packages in the current class path ⇐ in progress

Enumerate all jar files on the local PC, use a custom class loader to load each one, then get a list of all packages in each one ⇐ in progress

Spent 4 days so far trying to solve this problem that was solvable in .NET with 5 lines of code, and in Delphi with 3 lines of code

Investigate converting 409 jsp, and 498 java code files to ASP.net and C# ⇐ in progress

Give up on having automated unit, functional, and integration tests ⇐ in progress

Research Effort


Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • 1
    Since you specifically refer to the JUnit 5 test annotation, you may just use JUnit 5’s [`Launcher`](https://junit.org/junit5/docs/current/api/org.junit.platform.launcher/org/junit/platform/launcher/Launcher.html) to execute the tests. But if you really want to go low level, it’s [`ReflectionSupport`](https://junit.org/junit5/docs/current/api/org.junit.platform.commons/org/junit/platform/commons/support/ReflectionSupport.html) might have the right methods for you. – Holger Jul 22 '22 at 12:26
  • Unfortunately JUnit's test runner seems to require that all tests be in a special `package` that you know the name of. And reflection also seems to require you know the name of the package (or packages) that contain tests. The tests can be in any package; packages whose names i do not know. – Ian Boyd Jul 22 '22 at 12:58
  • There is the [**ServiceLoader**](https://docs.oracle.com/javase/6/docs/api/java/util/ServiceLoader.html) class, but that requires creating a configuration file – Ian Boyd Jul 22 '22 at 13:04
  • 1
    `ServiceLoader` is a nice thing when you use modules, as then, you don’t use configuration files anymore; it’s part of the language. E.g. you declare `provides service.Type with implementation.Type;` in the module-info and the compiler will verify that all constraints are met. But it wouldn’t help you with your specific case, as there is no service type (interface or abstract base class) extended by your test classes. – Holger Jul 22 '22 at 18:09
  • 1
    I don’t see that the `ReflectionSupport` I have linked would require you to know the package, e.g. [this method](https://junit.org/junit5/docs/current/api/org.junit.platform.commons/org/junit/platform/commons/support/ReflectionSupport.html#findAllClassesInClasspathRoot(java.net.URI,java.util.function.Predicate,java.util.function.Predicate)) only requires you to know the class path entry. – Holger Jul 22 '22 at 18:13
  • 2
    E.g., the following code scans the class path for a class containing a method annotated with `@Test`: `for(String cpEntry: System.getProperty("java.class.path").split(File.pathSeparator)) { for(var cl: ReflectionSupport.findAllClassesInClasspathRoot(Paths.get(cpEntry) .toUri(), cl -> Arrays.stream(cl.getMethods()).anyMatch(m -> m.isAnnotationPresent(Test.class)), str -> true)) { System.out.println(cl); } }` – Holger Jul 22 '22 at 18:15
  • 1
    "Give up on having automated unit, functional, and integration tests" << sounds like a bad idea. Some cynical side in me wants me to suggest you to just turn off all computers - computers that don't run can't be affected by any bugs. – Johannes Kuhn Jul 22 '22 at 22:50
  • @JohannesKuhn Oh i absolutely agree with you; which was why i spent 4 business days trying to get automated testing to work - rather than doing, you know, the actual work i'm supposed to be doing. It's ok though, when the code was originally written in 2003 it didn't have any automated tests. – Ian Boyd Jul 23 '22 at 17:05
  • 1
    Yeah, but other requirements are strange too: "Enumerate all jar files on the local PC, use a custom class loader to load each one, then get a list of all packages in each one" - I don't think it is possible to enumerate all assemblies in .NET. I'm not sure about the requirement that Reflections need the package names. – Johannes Kuhn Jul 23 '22 at 17:49
  • @JohannesKuhn There may be a disconnect in terminology *(i may not be using the right Java-words)*. But you can see the example code above. Someone just needs to figure out a way for the application to call them. In .NET the solution is trivial (as you can see in the code above). In Java it is not possible (for technical reasons of how the JRE handles classes and when classes become available for reflecting. It's not as elegant, or functional as .NET). But if you can post some code that can find those methods, spread across unknown names of Java *"packages"*: **please** be my guest. – Ian Boyd Jul 23 '22 at 18:43
  • 1
    This is a small POC that I wrote: https://pastebin.com/Nu17sKWs - it finds the annotation as expected. I had to add the `Scanners.MethodsAnnotated` so it would work. – Johannes Kuhn Jul 25 '22 at 13:39
  • That code seems to be getting the path to a specific .jar file. (`../testmod/target/testmod-0.0.1-SNAPSHOT.jar`) That falls apart when there are multiple jars whose names i you do not know. – Ian Boyd Jul 25 '22 at 14:13
  • 1
    Yeah, but you can simply scan the file system. You probably would have to do the same thing in .NET anyway. You may also use the current class path - although I did not look into how to do that with reflections. – Johannes Kuhn Jul 25 '22 at 14:36
  • @JohannesKuhn You don't have to do the name thing in .NET: you can see the entirety the 5-lines of .NET code that finds all methods. – Ian Boyd Jul 25 '22 at 15:00
  • 2
    Well, as your own comment already says, “Enumerate all assemblies *in the current application domain*”, so it’s not the magic tool that scans the entire hard drive. Replace “application domain” with “class path” and take the one-liner I posted in [this comment](https://stackoverflow.com/questions/73045692/how-to-call-all-methods-in-the-current-application-that-have-a-certain-annotati#comment129078739_73045692) two days ago. So, what’s the problem? – Holger Jul 25 '22 at 15:23
  • @Holgar [`class ReflectionSupport not found`](https://i.imgur.com/3R2HGcD.png) *(I fixed `var` being an invalid type, so that's not an issue anymore)* – Ian Boyd Jul 25 '22 at 17:59
  • 1
    Did you add the relevant dependency? – Johannes Kuhn Jul 25 '22 at 19:20
  • Sure did; you can see in the linked screenshot. – Ian Boyd Jul 25 '22 at 19:25
  • Ehm, you added JUnit? That is not reflections. – Johannes Kuhn Jul 26 '22 at 14:55
  • I did add JUnit, [because ReflectionSupport is part of JUnit, not Reflections](https://junit.org/junit5/docs/current/api/org.junit.platform.commons/org/junit/platform/commons/support/ReflectionSupport.html). – Ian Boyd Jul 26 '22 at 16:01

2 Answers2

1

Java and .Net are fundamentally different. The reason this task is not supported in the JVM out of the box is because of JVM's intrinsic lazy class loading; the JVM is not fully aware of every possible class on the classpath and loading all of them would be extraordinarily intensive CPU and Memory wise.

Essentially: What you are asking for is not possible without extensive writing of code that can scan avaialable classes on the classpath without loading them and examine their contents. You could implement this yourself, but it will be quite intensive. If you do wish to go that route, the answer you mentioned How to find annotated methods in a given package? is likely the easiest method, but will be non-optimal.

All is not lost though: I think the easiest way to accomplish the task you're wanting is to use a library like the following: https://github.com/ronmamo/reflections

The Reflections library is able to scan the classpath without actually performing a classload and is quite quick. I use this lib personally for writing frameworks and scanning for developer extensions on startup.

Example:

// MethodsAnnotated with @GetMapping
Set<Method> resources = reflections.get(MethodsAnnotated.with(GetMapping.class).as(Method.class));
Jonathan S. Fisher
  • 8,189
  • 6
  • 46
  • 84
  • What would be the Reflections way to do it – Ian Boyd Jul 20 '22 at 11:30
  • Reflections works internally by examining bytecode without performing an actual classload operation. It's a well written library. If you add the dependency to your project, you can use my code snippet above, but I would take a look at the documentation on the project page. Good luck! – Jonathan S. Fisher Jul 20 '22 at 13:58
  • Is it supposed to be `Reflections`; with a capital **R**? And where is `MethodsAnnotated` declared? Day three. I just want to run test cases; Java makes things so painful at every turn. – Ian Boyd Jul 20 '22 at 18:59
  • Oh, wait, from the homepage, *"Scanner must be configured in order to be queried, otherwise an empty result is returned"* I was hoping Reflections had solved all these problems. Back to trying to figure out how get JUnit to find its tests. – Ian Boyd Jul 20 '22 at 19:02
  • If Junit can't find all of it's tests, that's a configuration problem with your pom.xml! You can easily setup the surefire plugin to exclude or include certain tests – Jonathan S. Fisher Jul 20 '22 at 20:26
  • Well that makes sense; i don't *have* a pom.xml file! – Ian Boyd Jul 22 '22 at 03:42
  • Yes, that's why. JUnit looks by default for Tests by filename. You can configure this behavior by seting up the maven plugin to scan for other tests with different names or different annotations. – Jonathan S. Fisher Jul 22 '22 at 15:50
0

Use ClassGraph. Unlike Reflections, it's (exceptionally) actively maintained and works in many more scenarios (e.g. Reflections will keel over if you try to use it with modules).

try (ScanResult scanResult = new ClassGraph()
                               .enableAllInfo()  // Scan classes, methods, fields, annotations
                               .scan()) {        // Start the scan
      for (ClassInfo clazz : scanResult.getClassesWithMethodAnnotation(annotation)) {
          //Just your run-of-the-mill reflection from here
          Class<?> loaded = clazz.loadClass(); //SEE THE NOTE
          //Assumes the default constructor exists. Do what's appropariate if it doesn't.
          Object instance = loaded.getConstructor().newInstance(...);
          //Use the usual clazz.getMethods() and filter by annotation, or use ClassInfo to get closer
          Method annotatedMethod = ...;
          annotatedMethod.invoke(instance);
      }
}

NOTE: ClassGraph tries its best to figure out what classloader to use when loading classes, but you can still end up in bizarre situations. So, if you know already the correct classloader to use, I'd recommned going with the regular Java reflection here instead, e.g. Class.forName(clazz.getName(), loader).

kaqqao
  • 12,984
  • 10
  • 64
  • 118