Reflection, Annotations, Modules, and Metaprogramming
Understand reflection, annotations, dynamic proxies, modules, service loading, and the trade-offs of runtime metaprogramming.
Reflection
Reflection lets code inspect classes, methods, fields, constructors, and annotations at runtime.
Class<?> type = User.class;
for (var method : type.getDeclaredMethods()) {
System.out.println(method.getName());
}
Frameworks use reflection for dependency injection, serialization, ORM mapping, testing, and plugin systems. Reflection can weaken compile-time guarantees and complicate encapsulation, performance analysis, and native-image compatibility.
Use reflection at framework boundaries rather than spreading it through ordinary business logic.
Prefer normal Java calls for business logic; use reflection when the framework truly needs dynamic discovery.
- Reflection provides runtime type inspection and invocation.
- It trades some compile-time safety for flexibility.
- Framework infrastructure is a better home for reflection.
Annotations and Metadata
Annotations attach metadata to declarations. Frameworks can define custom annotations and inspect them at compile time or runtime.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audited {
String action();
}
Retention controls whether metadata is available in source, class files, or runtime reflection. Annotation processing can generate code during compilation and avoid some runtime reflection.
- Annotations are metadata, not executable behavior by themselves.
- Retention determines how long annotation information is available.
- Compile-time processing can move work out of runtime.
Dynamic Proxies and Service Loading
Java can create dynamic proxies around interfaces, enabling cross-cutting behavior such as logging, metrics, authorization, or transaction interception. Service loading provides a standard plugin discovery mechanism.
ServiceLoader<MyPlugin> plugins =
ServiceLoader.load(MyPlugin.class);
for (MyPlugin plugin : plugins) {
plugin.start();
}
These mechanisms are useful for extensibility but introduce indirection that should be documented.
- Dynamic proxies are primarily interface-oriented.
- Service loading supports pluggable implementations.
- Runtime indirection should be documented.
JPMS Modules and Strong Encapsulation
A module declares dependencies and exported packages.
module com.example.orders {
requires java.net.http;
exports com.example.orders.api;
}
exports controls package access across modules. opens is relevant when reflective access to otherwise encapsulated packages is required.
Packages organize names; modules define a larger dependency and encapsulation boundary.
- Packages and modules solve different levels of organization.
exportscontrols cross-module accessibility.opensis relevant to reflective access.