How Reflections Scans Classes
Updated at 1782720311000Here 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
SubTypesScannerindex, - 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.