This is an automated email from the ASF dual-hosted git repository.

He-Pin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-http.git


The following commit(s) were added to refs/heads/main by this push:
     new cc37ca0e4 Replace Reflections with ClassGraph in tests (#1132)
cc37ca0e4 is described below

commit cc37ca0e455f4cde4161c36df8dc3f7482a71141
Author: He-Pin(kerr) <[email protected]>
AuthorDate: Tue Jul 7 00:28:20 2026 +0800

    Replace Reflections with ClassGraph in tests (#1132)
    
    * refactor: replace java.lang.reflect with MethodHandle/VarHandle for JIT 
optimization
    
    Motivation:
    java.lang.reflect Method.invoke and Constructor.newInstance bypass JIT
    inlining, adding overhead to build-time aggregation calls and test code.
    
    Modification:
    - SbtInternalAccess: replace Method.invoke with MethodHandle.invoke via
      unreflect for Aggregation.showRun calls
    - Http2ServerSpec: replace Field.get with VarHandle.get via
      unreflectVarHandle for ManualProbe.probe field access
    - TurkishISpec: replace Constructor.newInstance with MethodHandle.invoke
      via unreflectConstructor for HttpCharsets$ instantiation
    
    Result:
    MethodHandle and VarHandle enable JIT inlining, eliminating reflection
    overhead in both build-time and test code paths.
    
    Tests:
    - sbt "http2-tests / Test / compile" - success
    - sbt "http-core / Test / compile" - success
    
    References:
    None - internal refactoring
    
    * refactor: use method handles in tests and sbt access
    
    Motivation:
    Reduce direct reflective invocation in HTTP tests and build helper code 
while keeping API consistency checks maintainable.
    
    Modification:
    Use bounded ClassGraph scans only for test API metadata, cache MethodHandle 
and VarHandle access where private or dynamic access is needed, and update test 
descriptors away from Class.forName.
    
    Result:
    HTTP reflective invocation sites are replaced with cached handles where 
practical, with metadata scanning limited to Pekko HTTP test packages.
    
    * fix: use invokeWithArguments() for no-arg calls and fix array class 
resolution
    
    - TurkishISpec: replace invoke() with invokeWithArguments() for Scala 3
      polymorphic signature compatibility
    - SerializabilitySpec: use Class.forName for array type descriptors in
      resolveClass, since MethodType.fromMethodDescriptorString can fail on
      some array descriptors during deserialization
    
    * refactor: scope reflection replacement to ClassGraph scans
    
    Motivation:
    PR review pointed out that replacing ordinary reflection with 
MethodHandle/VarHandle code added complexity without a demonstrated performance 
win. The intended change is to avoid Reflections' deep reflection during test 
classpath scans.
    
    Modification:
    Keep the ClassGraph migration for the documentation and directive 
consistency scanners, close ScanResult resources after each suite, and restore 
the unrelated MethodHandle/VarHandle changes to the simpler existing 
implementations.
    
    Result:
    The PR diff is focused on replacing org.reflections with ClassGraph 4.8.184 
in the relevant test scanners.
    
    Tests:
    - scalafmt --mode diff-ref=b0d3009b375b536397a776880301cc1943ef4645 - 
success
    - scalafmt --list --mode diff-ref=b0d3009b375b536397a776880301cc1943ef4645 
- success
    - git diff --check - success
    - JDK 17 sbt "docs / Test / testOnly docs.ApiMayChangeDocCheckerSpec" 
"http-tests / Test / testOnly 
org.apache.pekko.http.javadsl.DirectivesConsistencySpec" - success
    - qodercli -p --output-format stream-json --cwd 
"/Users/hepin/IdeaProjects/pekkos/pekko-http" --attachment 
/tmp/project-review.diff review - No must-fix findings
    
    References:
    Refs #1132
---
 .../scala/docs/ApiMayChangeDocCheckerSpec.scala    |  56 +++++---
 .../http/javadsl/DirectivesConsistencySpec.scala   | 151 ++++++++++++++-------
 project/Dependencies.scala                         |   8 +-
 3 files changed, 143 insertions(+), 72 deletions(-)

diff --git a/docs/src/test/scala/docs/ApiMayChangeDocCheckerSpec.scala 
b/docs/src/test/scala/docs/ApiMayChangeDocCheckerSpec.scala
index d2be06ea7..19d917c3f 100644
--- a/docs/src/test/scala/docs/ApiMayChangeDocCheckerSpec.scala
+++ b/docs/src/test/scala/docs/ApiMayChangeDocCheckerSpec.scala
@@ -13,29 +13,41 @@
 
 package docs
 
-import java.lang.reflect.Method
-
 import org.apache.pekko.annotation.ApiMayChange
-import org.reflections.Reflections
-import org.reflections.scanners.{ MethodAnnotationsScanner, Scanners, 
TypeAnnotationsScanner }
-import org.reflections.util.{ ClasspathHelper, ConfigurationBuilder }
+import io.github.classgraph.{ ClassGraph, MethodInfo }
 import org.scalatest.Assertion
 
-import scala.collection.mutable
 import scala.io.Source
 import scala.jdk.CollectionConverters._
+import org.scalatest.BeforeAndAfterAll
 import org.scalatest.matchers.should.Matchers
 import org.scalatest.wordspec.AnyWordSpec
 
-class ApiMayChangeDocCheckerSpec extends AnyWordSpec with Matchers {
+class ApiMayChangeDocCheckerSpec extends AnyWordSpec with Matchers with 
BeforeAndAfterAll {
+
+  private val apiMayChangeAnnotation = classOf[ApiMayChange].getName
+  private val httpPackagePrefix = "org.apache.pekko.http."
+
+  private lazy val scanResult =
+    new ClassGraph()
+      .acceptPackages("org.apache.pekko.http")
+      .enableClassInfo()
+      .enableMethodInfo()
+      .enableAnnotationInfo()
+      .ignoreClassVisibility()
+      .ignoreMethodVisibility()
+      .scan()
 
-  def prettifyName(clazz: Class[?]): String = {
-    clazz.getCanonicalName.replaceAll("\\$minus", "-").split("\\$")(0)
+  def prettifyName(className: String): String = {
+    className.replaceAll("\\$minus", "-").split("\\$")(0)
   }
 
+  private def isHttpClass(className: String): Boolean =
+    className.startsWith(httpPackagePrefix)
+
   // As Specs, Directives and HttpApp inherit get all directives methods, we 
skip those as they are not really bringing any extra info
-  def removeClassesToIgnore(method: Method): Boolean = {
-    Seq("Spec", ".Directives", 
".HttpApp").exists(method.getDeclaringClass.getCanonicalName.contains)
+  def removeClassesToIgnore(method: MethodInfo): Boolean = {
+    Seq("Spec", ".Directives", ".HttpApp").exists(method.getClassName.contains)
   }
 
   def collectMissing(docPage: Seq[String])(set: Set[String], name: String): 
Set[String] = {
@@ -56,30 +68,34 @@ class ApiMayChangeDocCheckerSpec extends AnyWordSpec with 
Matchers {
   }
 
   "compatibility-guidelines.md doc page" should {
-    val reflections = new Reflections(new ConfigurationBuilder()
-      .setUrls(ClasspathHelper.forPackage("org.apache.pekko.http"))
-      .setScanners(
-        Scanners.TypesAnnotated,
-        Scanners.MethodsAnnotated))
     val source = 
Source.fromFile("docs/src/main/paradox/compatibility-guidelines.md")
     try {
       val docPage = source.getLines().toList
       "contain all ApiMayChange references in classes" in {
-        val classes: mutable.Set[Class[?]] = 
reflections.getTypesAnnotatedWith(classOf[ApiMayChange], true).asScala
+        val classes = 
scanResult.getClassesWithAnnotation(apiMayChangeAnnotation).asScala.filter(classInfo
 =>
+          isHttpClass(classInfo.getName))
         val missing = classes
-          .map(prettifyName)
+          .map(classInfo => prettifyName(classInfo.getName))
           .foldLeft(Set.empty[String])(collectMissing(docPage))
         checkNoMissingCases(missing, "Types")
       }
       "contain all ApiMayChange references in methods" in {
-        val methods = 
reflections.getMethodsAnnotatedWith(classOf[ApiMayChange]).asScala
+        val methods =
+          
scanResult.getClassesWithMethodAnnotation(apiMayChangeAnnotation).asScala.flatMap
 { classInfo =>
+            classInfo.getDeclaredMethodInfo.asScala.filter(method =>
+              isHttpClass(method.getClassName) && 
method.hasAnnotation(apiMayChangeAnnotation))
+          }
         val missing = methods
           .filterNot(removeClassesToIgnore)
-          .map(method => prettifyName(method.getDeclaringClass) + "#" + 
method.getName)
+          .map(method => prettifyName(method.getClassName) + "#" + 
method.getName)
           .foldLeft(Set.empty[String])(collectMissing(docPage))
         checkNoMissingCases(missing, "Methods")
       }
     } finally source.close()
 
   }
+
+  override protected def afterAll(): Unit =
+    try scanResult.close()
+    finally super.afterAll()
 }
diff --git 
a/http-tests/src/test/scala/org/apache/pekko/http/javadsl/DirectivesConsistencySpec.scala
 
b/http-tests/src/test/scala/org/apache/pekko/http/javadsl/DirectivesConsistencySpec.scala
index 5c289773d..78a514184 100644
--- 
a/http-tests/src/test/scala/org/apache/pekko/http/javadsl/DirectivesConsistencySpec.scala
+++ 
b/http-tests/src/test/scala/org/apache/pekko/http/javadsl/DirectivesConsistencySpec.scala
@@ -13,18 +13,80 @@
 
 package org.apache.pekko.http.javadsl
 
-import java.lang.reflect.{ Method, Modifier }
-
+import scala.jdk.CollectionConverters._
 import scala.util.control.NoStackTrace
 
+import io.github.classgraph.{ ClassGraph, MethodInfo => ClassGraphMethodInfo }
 import org.apache.pekko
-import pekko.http.javadsl.server.directives.CorrespondsTo
 
+import org.scalatest.BeforeAndAfterAll
 import org.scalatest.exceptions.TestPendingException
 import org.scalatest.matchers.should.Matchers
 import org.scalatest.wordspec.AnyWordSpec
 
-class DirectivesConsistencySpec extends AnyWordSpec with Matchers {
+object ClassGraphMembers {
+  final case class MethodInfo(name: String, isStatic: Boolean, correspondsTo: 
Option[String])
+
+  private val correspondsToAnnotation = 
"org.apache.pekko.http.javadsl.server.directives.CorrespondsTo"
+
+  private lazy val scanResult =
+    new ClassGraph()
+      .acceptPackages("org.apache.pekko.http")
+      .enableClassInfo()
+      .enableMethodInfo()
+      .enableAnnotationInfo()
+      .ignoreClassVisibility()
+      .ignoreMethodVisibility()
+      .scan()
+
+  private lazy val publicMethodsCache =
+    collection.concurrent.TrieMap.empty[String, Vector[MethodInfo]]
+  private lazy val declaredMethodsCache =
+    collection.concurrent.TrieMap.empty[String, Vector[MethodInfo]]
+  private lazy val interfacesCache =
+    collection.concurrent.TrieMap.empty[String, Vector[String]]
+  private lazy val superclassesCache =
+    collection.concurrent.TrieMap.empty[String, Vector[String]]
+
+  def publicMethods(clazz: Class[?]): Array[MethodInfo] =
+    publicMethodsCache
+      .getOrElseUpdate(clazz.getName,
+        
classInfo(clazz.getName).getMethodInfo.asScala.filter(_.isPublic).map(toMethod).toVector)
+      .toArray
+
+  def declaredMethods(className: String): Vector[MethodInfo] =
+    declaredMethodsCache.getOrElseUpdate(
+      className,
+      
classInfo(className).getDeclaredMethodInfo.asScala.map(toMethod).toVector)
+
+  def allInterfaces(clazz: Class[?]): Vector[String] =
+    interfacesCache.getOrElseUpdate(clazz.getName,
+      
classInfo(clazz.getName).getInterfaces.directOnly.asScala.map(_.getName).toVector)
+
+  def superclasses(clazz: Class[?]): Vector[String] =
+    superclassesCache.getOrElseUpdate(
+      clazz.getName,
+      (classInfo(clazz.getName) +: 
classInfo(clazz.getName).getSuperclasses.asScala.toVector)
+        .map(_.getName)
+        .filterNot(_ == "java.lang.Object"))
+
+  private def classInfo(className: String) =
+    Option(scanResult.getClassInfo(className))
+      .getOrElse(throw new IllegalArgumentException(s"Could not find class 
metadata for [$className]"))
+
+  def close(): Unit =
+    scanResult.close()
+
+  private def toMethod(method: ClassGraphMethodInfo): MethodInfo =
+    MethodInfo(
+      method.getName,
+      method.isStatic,
+      Option(method.getAnnotationInfo(correspondsToAnnotation))
+        .flatMap(annotation => 
Option(annotation.getParameterValues.getValue("value")).map(_.toString)))
+}
+
+class DirectivesConsistencySpec extends AnyWordSpec with Matchers with 
BeforeAndAfterAll {
+  import ClassGraphMembers.MethodInfo
 
   val scalaDirectivesClazz = classOf[pekko.http.scaladsl.server.Directives]
   val javaDirectivesClazz = classOf[pekko.http.javadsl.server.AllDirectives]
@@ -37,30 +99,30 @@ class DirectivesConsistencySpec extends AnyWordSpec with 
Matchers {
     Set("not", "DoubleNumber", "HexIntNumber", "HexLongNumber", "IntNumber", 
"JavaUUID", "LongNumber",
       "Neutral", "PathEnd", "Remaining", "Segment", "Segments", "Slash", 
"RemainingPath") // TODO do we cover these?
 
-  def prepareDirectivesList(in: Array[Method]): List[Method] = {
-    in.toSet[Method]
+  def prepareDirectivesList(in: Array[MethodInfo]): List[MethodInfo] = {
+    in.toSet
       .toList
-      .foldLeft[List[Method]](Nil) {
+      .foldLeft[List[MethodInfo]](Nil) {
         (l, s) =>
           {
-            val test = l.find { _.getName.toLowerCase == s.getName.toLowerCase 
}
+            val test = l.find { _.name.toLowerCase == s.name.toLowerCase }
             if (test.isEmpty) s :: l else l
           }
       }
-      .sortBy(_.getName)
+      .sortBy(_.name)
       .iterator
-      .filterNot(m => Modifier.isStatic(m.getModifiers))
-      .filterNot(m => ignore(m.getName))
-      .filterNot(m => m.getName.contains("$"))
-      .filterNot(m => m.getName.startsWith("_"))
+      .filterNot(_.isStatic)
+      .filterNot(m => ignore(m.name))
+      .filterNot(m => m.name.contains("$"))
+      .filterNot(m => m.name.startsWith("_"))
       .toList
   }
 
   val scalaDirectives = {
-    prepareDirectivesList(scalaDirectivesClazz.getMethods)
+    
prepareDirectivesList(ClassGraphMembers.publicMethods(scalaDirectivesClazz))
   }
   val javaDirectives = {
-    prepareDirectivesList(javaDirectivesClazz.getMethods)
+    prepareDirectivesList(ClassGraphMembers.publicMethods(javaDirectivesClazz))
   }
 
   val correspondingScalaMethods = {
@@ -68,9 +130,8 @@ class DirectivesConsistencySpec extends AnyWordSpec with 
Matchers {
       for {
         // using Scala annotations - Java annotations were magically not 
present in certain places...
         d <- javaDirectives
-        if d.isAnnotationPresent(classOf[CorrespondsTo])
-        annot = d.getAnnotation(classOf[CorrespondsTo])
-      } yield d.getName -> annot.value()
+        correspondent <- d.correspondsTo
+      } yield d.name -> correspondent
 
     Map(javaToScalaMappings.toList: _*)
   }
@@ -78,17 +139,17 @@ class DirectivesConsistencySpec extends AnyWordSpec with 
Matchers {
   val correspondingJavaMethods = Map() ++ correspondingScalaMethods.map(_.swap)
 
   /** Left(@CorrespondsTo(...) or Right(normal name) */
-  def correspondingScalaMethodName(m: Method): Either[String, String] =
-    correspondingScalaMethods.get(m.getName) match {
+  def correspondingScalaMethodName(m: MethodInfo): Either[String, String] =
+    correspondingScalaMethods.get(m.name) match {
       case Some(correspondent) => Left(correspondent)
-      case _                   => Right(m.getName)
+      case _                   => Right(m.name)
     }
 
   /** Left(@CorrespondsTo(...) or Right(normal name) */
-  def correspondingJavaMethodName(m: Method): Either[String, String] =
-    correspondingJavaMethods.get(m.getName) match {
+  def correspondingJavaMethodName(m: MethodInfo): Either[String, String] =
+    correspondingJavaMethods.get(m.name) match {
       case Some(correspondent) => Left(correspondent)
-      case _                   => Right(m.getName)
+      case _                   => Right(m.name)
     }
 
   val allowMissing: Map[Class[?], Set[String]] = Map(
@@ -122,7 +183,8 @@ class DirectivesConsistencySpec extends AnyWordSpec with 
Matchers {
   def assertHasMethod(c: Class[?], name: String, alternativeName: String): 
Unit = {
     // include class name to get better error message
     if (!allowMissing.getOrElse(c, Set.empty).exists(n => n == name || n == 
alternativeName)) {
-      val methods = c.getMethods.collect { case m if !ignore(m.getName) => 
c.getName + "." + m.getName }
+      val methods =
+        ClassGraphMembers.publicMethods(c).collect { case m if !ignore(m.name) 
=> c.getName + "." + m.name }
 
       def originClazz = {
         // look in the "opposite" class
@@ -130,32 +192,20 @@ class DirectivesConsistencySpec extends AnyWordSpec with 
Matchers {
         // in hava we have a huge inheritance chain so we unfold it
         c match {
           case `javaDirectivesClazz` =>
-            val all = scalaDirectivesClazz
             (for {
-              i <- all.getInterfaces
-              m <- i.getDeclaredMethods
-              if m.getName == name || m.getName == alternativeName
+              i <- ClassGraphMembers.allInterfaces(scalaDirectivesClazz)
+              m <- ClassGraphMembers.declaredMethods(i)
+              if m.name == name || m.name == alternativeName
             } yield i).headOption
-              .map(_.getName)
-              .getOrElse(throw new Exception(s"Unable to locate method [$name] 
on source class $all"))
+              .getOrElse(throw new Exception(s"Unable to locate method [$name] 
on source class $scalaDirectivesClazz"))
 
           case `scalaDirectivesClazz` =>
-            val all = javaDirectivesClazz
-
-            var is = List.empty[Class[?]]
-            var c: Class[?] = all
-            while (c != classOf[java.lang.Object]) {
-              is = c :: is
-              c = c.getSuperclass
-            }
-
             (for {
-              i <- is
-              m <- i.getDeclaredMethods
-              if m.getName == name || m.getName == alternativeName
+              i <- ClassGraphMembers.superclasses(javaDirectivesClazz)
+              m <- ClassGraphMembers.declaredMethods(i)
+              if m.name == name || m.name == alternativeName
             } yield i).headOption
-              .map(_.getName)
-              .getOrElse(throw new Exception(s"Unable to locate method [$name] 
on source class $all"))
+              .getOrElse(throw new Exception(s"Unable to locate method [$name] 
on source class $javaDirectivesClazz"))
         }
       }
 
@@ -170,8 +220,8 @@ class DirectivesConsistencySpec extends AnyWordSpec with 
Matchers {
   }
 
   "DSL Stats" should {
-    info("Scala Directives: ~" + 
scalaDirectives.map(_.getName).filterNot(ignore).size)
-    info("Java Directives: ~" + 
javaDirectives.map(_.getName).filterNot(ignore).size)
+    info("Scala Directives: ~" + 
scalaDirectives.map(_.name).filterNot(ignore).size)
+    info("Java Directives: ~" + 
javaDirectives.map(_.name).filterNot(ignore).size)
   }
 
   "Directive aliases" should {
@@ -182,7 +232,7 @@ class DirectivesConsistencySpec extends AnyWordSpec with 
Matchers {
   "Consistency scaladsl -> javadsl" should {
     for {
       m <- scalaDirectives
-      name = m.getName
+      name = m.name
       targetName = correspondingJavaMethodName(m) match {
         case Left(l)  => l
         case Right(r) => r
@@ -196,7 +246,7 @@ class DirectivesConsistencySpec extends AnyWordSpec with 
Matchers {
   "Consistency javadsl -> scaladsl" should {
     for {
       m <- javaDirectives
-      name = m.getName
+      name = m.name
       targetName = correspondingScalaMethodName(m) match {
         case Left(l)  => l
         case Right(r) => r
@@ -207,4 +257,7 @@ class DirectivesConsistencySpec extends AnyWordSpec with 
Matchers {
     }
   }
 
+  override protected def afterAll(): Unit =
+    try ClassGraphMembers.close()
+    finally super.afterAll()
 }
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index daf2cbbe6..2a513523d 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -29,6 +29,7 @@ object Dependencies {
   val h2specArtifactExtension = if (h2specExe.endsWith("exe")) "zip" else 
"tar.gz"
   val h2specUrl =
     
s"https://github.com/summerwind/h2spec/releases/download/v$h2specVersion/$h2specName.$h2specArtifactExtension";
+  val classGraphVersion = "4.8.184"
 
   val scalaTestVersion = "3.2.20"
   val scalaCheckVersion = "1.19.0"
@@ -74,7 +75,7 @@ object Dependencies {
       val sprayJson = Compile.sprayJson % "test"
       val gson = "com.google.code.gson" % "gson" % "2.14.0" % "test"
       val jacksonXml = "com.fasterxml.jackson.dataformat" % 
"jackson-dataformat-xml" % jacksonXmlVersion % "test"
-      val reflections = "org.reflections" % "reflections" % "0.10.2" % "test"
+      val classGraph = "io.github.classgraph" % "classgraph" % 
classGraphVersion % "test"
     }
 
     object Test {
@@ -90,6 +91,7 @@ object Dependencies {
       val scalatest = "org.scalatest" %% "scalatest" % scalaTestVersion % 
"test"
       val scalatestplusScalacheck = "org.scalatestplus" %% "scalacheck-1-19" % 
(scalaTestVersion + ".0") % "test"
       val scalatestplusJUnit = "org.scalatestplus" %% "junit-4-13" % 
(scalaTestVersion + ".0") % "test"
+      val classGraph = "io.github.classgraph" % "classgraph" % 
classGraphVersion % "test"
 
       // HTTP/2
 
@@ -149,7 +151,7 @@ object Dependencies {
     l ++= Seq(Test.munit % "provided; test")
 
   lazy val httpTests = Seq(
-    l ++= junitJupiterTestDeps.value ++ Seq(Test.scalatest))
+    l ++= junitJupiterTestDeps.value ++ Seq(Test.scalatest, Test.classGraph))
 
   lazy val httpXml = Seq(
     versionDependentDeps(scalaXml),
@@ -170,7 +172,7 @@ object Dependencies {
       Docs.sprayJson,
       Docs.gson,
       Docs.jacksonXml,
-      Docs.reflections) ++ junitJupiterTestDeps.value)
+      Docs.classGraph) ++ junitJupiterTestDeps.value)
 }
 
 object DependencyHelpers {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to