How Reflections Scans Classes

Updated at 1782720311000
Here is the full English version:
How Reflections Scans Java Classes

Overview

Reflections is a runtime metadata analysis library. Instead of scanning the whole classpath every time you need to find a class, annotation, method, or resource, Reflections scans configured locations once, extracts metadata from class files, and stores the result in searchable indexes.
The core idea is:
  • classpath locations are resolved into URLs,
  • each URL is opened through a virtual file system,
  • files are iterated and filtered,
  • scanners extract the metadata they care about,
  • results are stored in Store,
  • later query APIs read from those indexes.
flowchart TD
    A[Reflections configuration] --> B[URLs to scan]
    B --> C[VFS opens directory or jar]
    C --> D[Iterate files]
    D --> E{Accepted by input filter?}
    E -- No --> F[Skip]
    E -- Yes --> G[Scanner processes file]
    G --> H[MetadataAdapter reads class metadata]
    H --> I[Store saves key -> value]
    I --> J[Query APIs read indexed results]
VFS: Virtual File System.

Configuration Defines the Scan Scope

Reflections does not blindly scan everything. It scans according to a Configuration, usually built with ConfigurationBuilder.
A scan configuration contains:
  • urls: locations to scan, such as class directories, jars, classpath entries, packages, or explicit URLs.
  • scanners: scanner instances that will run on each class or resource.
  • inputsFilter: a filter for relative paths or fully qualified names.
  • metadataAdapter: the component used to read metadata from classes.
  • executorService: optional executor for parallel scanning.
  • expandSuperTypes: whether to expand missing supertype relationships after scanning.
A convenient constructor such as:
new Reflections("com.example");
effectively creates a configuration that:
  • finds URLs containing the package com.example,
  • adds an input filter for that package,
  • uses the default scanners for type annotations and subtypes.

From Package to URLs

When a package is provided, Reflections uses the class loader to find the corresponding resource. For example:
com.example.app
is converted into:
com/example/app
The class loader then returns URLs that contain that resource. A URL may point to:
  • a build output directory such as target/classes,
  • a jar file,
  • a jar: URL,
  • a servlet environment,
  • an application server VFS,
  • entries from java.class.path.
This is an important detail: Reflections scans URLs, not Java packages directly. A package is mainly a convenient way to discover physical locations on the classpath.

What VFS Means

VFS stands for Virtual File System. In Reflections, it is an abstraction layer that lets different kinds of classpath locations look like the same kind of “virtual directory”.
Java classes may live in many places: a real directory, a jar file, a jar: URL, a JBoss VFS location, or an OSGi-like bundle runtime. Without VFS, the scan logic would need separate code paths for each storage type.
Reflections solves this by converting each URL into a Vfs.Dir. From a Vfs.Dir, it can iterate over Vfs.File objects. The scanner layer does not need to know whether the file came from a directory or from inside a jar.
flowchart TD
    A[Input URL] --> B[VFS detects URL type]
    B --> C[File system directory]
    B --> D[Jar file]
    B --> E[Jar URL]
    B --> F[JBoss VFS / bundle]
    C --> G[Vfs.Dir]
    D --> G
    E --> G
    F --> G
    G --> H[Vfs.File]
    H --> I[Scanner reads class/resource]
In short, VFS is Reflections’ file-access adapter. It hides the differences between directories, jars, and special URL types behind one unified API.

How Reflections Finds Class Files

Reflections does not ask the JVM, “which classes exist?”. Instead, it treats the classpath as a set of files.
The flow is:
flowchart TD
    A[Configured package / classpath / URL] --> B[ClasspathHelper finds URLs]
    B --> C[VFS opens URL as virtual directory]
    C --> D[Iterate all Vfs.File entries]
    D --> E{Accepted by input filter?}
    E -- No --> F[Skip]
    E -- Yes --> G{Accepted by scanner?}
    G -- Class scanner --> H[Accept .class files]
    G -- ResourcesScanner --> I[Accept non-.class files]
    H --> J[MetadataAdapter reads class metadata]
    I --> K[Store resource path]
For class-related scanners such as subtype, type annotation, method annotation, and field annotation scanners, input acceptance goes through the metadata adapter. The default adapter accepts files ending with:
file.endsWith(".class")
So these scanners only process .class files.
ResourcesScanner does the opposite:
return !file.endsWith(".class");
That scanner is used for resources such as .xml, .properties, and other non-class files.

Filtering Inputs Before Scanning

Each file has a relative path, for example:
com/example/UserService.class
Reflections also creates a dot-style form:
com.example.UserService.class
The configured inputsFilter is checked against both forms. If the filter rejects the file, it is skipped.
This greatly reduces scan cost. If you only need com.example, you should avoid scanning the entire dependency classpath.

Javassist Reads Metadata from .class Files

In this codebase, Reflections prefers Javassist for reading metadata.
ConfigurationBuilder#getMetadataAdapter() first tries to create a JavassistAdapter. If that succeeds, scanners use it. If it fails, Reflections falls back to JavaReflectionAdapter.
With Javassist, the adapter opens the Vfs.File input stream and parses the bytecode as a ClassFile:
inputStream = file.openInputStream();
DataInputStream dis = new DataInputStream(new BufferedInputStream(inputStream));
return new ClassFile(dis);
After that, scanners can read metadata such as:
  • class name,
  • superclass,
  • interfaces,
  • fields,
  • methods,
  • annotations,
  • parameter types,
  • return type.
This means the normal path is:
VFS finds .class -> Javassist reads bytecode -> scanner extracts metadata -> Store saves index

Why Bytecode Metadata Reading Is Faster Than Reflection

Javassist reads metadata directly from .class bytecode as static data. Java Reflection usually requires resolving or loading a class into the JVM first, then querying metadata from a Class<?>.
Conceptually:
Javassist:
.class file -> parse constant pool / attributes / descriptors -> metadata

Reflection:
class name -> class loader resolves/loads class -> Class<?> -> reflection API -> metadata
Javassist is often faster and lighter for bulk scanning because:
  • it does not need to define/load the class into the JVM,
  • it avoids much of the class loader and dependency resolution work,
  • it can often read names of annotations, supertypes, interfaces, and descriptors even if some related classes are not loadable,
  • it fits the use case of scanning many classes for metadata only,
  • it avoids bringing scanned classes into the runtime type space just to inspect their structure.
For example, to know whether UserService has @Service, Javassist only needs to read annotation attributes from the .class file. Reflection needs a Class<UserService> first, then calls APIs such as getDeclaredAnnotations().
flowchart TD
    A[Scan many .class files] --> B{Metadata reading strategy}
    B --> C[Javassist reads bytecode]
    B --> D[Reflection loads Class]
    C --> E[Parse class name, annotations, method descriptors]
    D --> F[ClassLoader resolves class and dependencies]
    F --> G[Reflection API reads metadata]
    E --> H[Save into Store]
    G --> H
For Reflections, the goal is to index metadata, not execute classes. Reading bytecode directly is therefore a better fit than reflection for the scan phase.

Scanners Create Indexes

Each scanner is responsible for one kind of metadata. All scanners write data into Store as:
index name -> key -> values
Examples:
SubTypesScanner:
    com.example.BaseService -> com.example.UserService

TypeAnnotationsScanner:
    org.springframework.stereotype.Service -> com.example.UserService

MethodAnnotationsScanner:
    javax.annotation.PostConstruct -> com.example.UserService.init()

FieldAnnotationsScanner:
    javax.inject.Inject -> com.example.UserService.repository
Important scanners include:
  • SubTypesScanner: stores supertype/interface -> subtype.
  • TypeAnnotationsScanner: stores annotation -> annotated class.
  • MethodAnnotationsScanner: stores annotation -> method/constructor.
  • FieldAnnotationsScanner: stores annotation -> field.
  • MethodParameterScanner: stores parameter types, return type, and parameter annotations.
  • ResourcesScanner: stores non-class resources.
  • TypeElementsScanner: stores fields, methods, and annotations per type.
  • MemberUsageScanner: uses Javassist to find method, field, and constructor usage.
flowchart TD
    A[Class metadata] --> B[SubTypesScanner]
    A --> C[TypeAnnotationsScanner]
    A --> D[MethodAnnotationsScanner]
    A --> E[FieldAnnotationsScanner]
    A --> F[MethodParameterScanner]

    B --> G[supertype -> subtype]
    C --> H[annotation -> class]
    D --> I[annotation -> method]
    E --> J[annotation -> field]
    F --> K[signature / return / parameter annotation -> method]

Each Class File Is Parsed Once Per Scan Pass

When scanning a .class file, multiple scanners may need the same class metadata. Reflections passes a shared classObject between scanners.
sequenceDiagram
    participant R as Reflections
    participant S1 as Scanner 1
    participant S2 as Scanner 2
    participant A as MetadataAdapter
    participant Store as Store

    R->>S1: scan(file, classObject=null)
    S1->>A: parse class file
    A-->>S1: classObject
    S1->>Store: put key/value
    S1-->>R: classObject

    R->>S2: scan(file, classObject)
    S2->>Store: put key/value
    S2-->>R: classObject
This avoids parsing the same class file again for every scanner.

Store Is the Query Backbone

Store is a collection of multimaps. Each scanner owns a separate index. Query APIs do not scan the classpath again; they read from Store.
For example, getSubTypesOf(Base.class):
  • reads the SubTypesScanner index,
  • finds values under the key Base,
  • recursively follows subtype relationships,
  • resolves class names into Class<?>.
Annotation queries work similarly. getTypesAnnotatedWith(Service.class) reads the TypeAnnotationsScanner index, then may combine it with subtype data when handling meta-annotations or inherited annotations.

Expanding Supertype Relationships

After scanning, if expandSuperTypes is enabled, Reflections can add missing supertype relationships.
For example, scanning may discover:
B -> C
But if B extends A and A was outside the scanned package, the store may not yet contain:
A -> B
Reflections can use reflection utilities to add that relationship. This makes subtype queries more complete even when not every ancestor package was scanned.

Parallel Scanning

If an ExecutorService is configured, Reflections scans URLs in parallel. Each URL is submitted as a task. In that mode, Store uses synchronized multimaps to support concurrent writes.
After all tasks finish, the executor is shut down.
flowchart TD
    A[URL list] --> B{ExecutorService configured?}
    B -- No --> C[Scan URLs sequentially]
    B -- Yes --> D[Submit each URL as a task]
    D --> E[Wait for Futures]
    C --> F[Store]
    E --> F[Synchronized Store]

What Reflections Does Not Do

Reflections does not compile source code and does not analyze .java files. It mainly works with .class files and resources available on the classpath.
It also does not magically know which metadata you want. The relevant scanner must be configured. For example:
  • field annotation queries require FieldAnnotationsScanner,
  • resource queries require ResourcesScanner,
  • method parameter queries require MethodParameterScanner.
Results also depend on scan scope. If you scan only a narrow package, relationships or annotations outside that scope may be missing, except where expandSuperTypes can compensate.

Conclusion

Reflections works as a metadata indexing pipeline:
classpath/package -> URL -> VFS files -> filter -> scanner -> Store -> query
This design separates the expensive scan phase from the fast query phase. Scanners make the system flexible: the same classpath traversal can produce indexes for subtypes, annotations, methods, fields, parameters, member usage, or resources.
The key to using Reflections well is correct configuration: the right URLs, the right input filter, and the right scanners. When those three pieces match your use case, Reflections provides a compact and powerful way to discover the structure of a Java application’s classpath.

Table Of Contents