Unlock Java’s Power: Mastering Reflection for Dynamic Code
This article explains Java reflection’s core concepts, how the JVM dynamically loads Class objects, and provides practical examples—including field retrieval, caching strategies, and integration with Spring—to help developers harness reflection for dynamic code manipulation while avoiding common pitfalls.
Background
Reflection in Java is a crucial feature that distinguishes Java from many other languages. Technologies such as AOP and dynamic proxies rely on reflection and bytecode manipulation, allowing non‑intrusive enhancements. However, business logic should not be placed inside AOP or proxies.
AOP: add logic before/after method execution, can decide whether to run the method.
Dynamic proxy: generate a proxy at runtime to enhance the target class.
This article summarizes the principles and practical usage of reflection.
What is Reflection?
Official definition:
Reflection is a feature in the Java programming language. It allows an executing Java program to examine or "introspect" upon itself, and manipulate internal properties of the program. For example, it's possible for a Java class to obtain the names of all its members and display them.
In other words, reflection lets a running Java program inspect and modify its own internal structure.
The term “reflection” is analogous to looking at oneself in a mirror.
How Reflection Works
In short, the JVM dynamically loads a Class object that contains complete metadata (package, name, fields, methods, superclass, interfaces, etc.). Obtaining a Class instance enables access to all that information.
Dynamic loading means the JVM loads a class into memory only when it is first referenced, creating a single Class instance that is shared by all objects of that type.
For more details, see the referenced tutorial.
Using Reflection
Key classes: Class, Field, Method, Constructor, and Parameter. Anything that appears in a Java object can be found in the java.reflect package.
Class
Three ways to obtain a Class object:
ClassName.class
object.getClass()
Class.forName("full.class.Name")
Field
ClassInstance.getField(name) – public field, includes superclasses.
ClassInstance.getDeclaredField(name) – declared field, excludes superclasses (common).
Field[] getFields() – all public fields, including superclasses (rare).
Field[] getDeclaredFields() – all declared fields, excludes superclasses (common).
Method
ClassInstance.getMethod(name, Class…) – public method, includes superclasses.
ClassInstance.getDeclaredMethod(name, Class…) – declared method, excludes superclasses.
Method[] getMethods() – all public methods, includes superclasses (common).
Method[] getDeclaredMethods() – all declared methods, excludes superclasses.
AnnotatedElement
Class, Field, Method inherit from AnnotatedElement, providing methods such as:
getAnnotation(Class) – retrieve a specific annotation.
isAnnotationPresent(Class) – check if an annotation is present.
getAnnotations() – obtain all annotations.
Reflection Examples
1. Retrieve all fields of a class
If a class lacks a toString() method or private fields lack getters, reflection can enumerate and modify those fields.
field.get(object) – obtain field value.
field.setAccessible(true) – make private field accessible.
field.set(Object, Object) – modify field value.
To obtain fields from superclasses, iterate with getSuperclass until reaching Object.
// Get all fields of a class, including superclasses
private List<Field> getAllFields(Class<?> clazz) {
List<Field> result = new ArrayList<>();
Class<?> cls = clazz;
while (cls != null) {
result.addAll(Arrays.asList(cls.getDeclaredFields()));
cls = cls.getSuperclass();
}
return result;
}Caching reflection data improves performance because reflection is costly.
Using breadth‑first search to collect fields of referenced classes
<code/**
* Example: retrieve all reflective fields of a class (including referenced classes)
*/
public class GetAllFields {
// Build a cache: key = fully qualified class name, value = list of its fields
public static Map<String, List<Field>> buildReflectCache(Class<?> clazz) {
Map<String, List<Field>> result = new HashMap<>();
List<Field> topLevelFields = getAllFields(clazz);
result.put(clazz.getName(), topLevelFields);
// BFS over fields
Queue<Field> queue = new LinkedList<>(topLevelFields);
while (!queue.isEmpty()) {
Field field = queue.poll();
// Handle collections or maps to extract generic types
if (Collection.class.isAssignableFrom(field.getType())
|| Map.class.isAssignableFrom(field.getType())) {
Type genericType = field.getGenericType();
if (genericType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) genericType;
for (Type type : parameterizedType.getActualTypeArguments()) {
Class<?> actualClass = (Class<?>) type;
if (!isBasicClass(actualClass)) {
List<Field> subFields = getAllFields(actualClass);
result.putIfAbsent(actualClass.getName(), subFields);
queue.addAll(subFields);
}
}
}
} else if (!isBasicClass(field.getType())) {
// Process custom types only
List<Field> subFields = getAllFields(field.getType());
result.putIfAbsent(field.getType().getName(), subFields);
queue.addAll(subFields);
}
}
return result;
}
// Retrieve all fields of a class, including superclasses
private static List<Field> getAllFields(Class<?> clazz) {
List<Field> result = new ArrayList<>();
Class<?> cls = clazz;
while (cls != null) {
result.addAll(Arrays.asList(cls.getDeclaredFields()));
cls = cls.getSuperclass();
}
return result;
}
// Determine if a class is a Java primitive/well‑known type
private static boolean isBasicClass(Class<?> clazz) {
return clazz != null && clazz.getClassLoader() == null;
}
public static void main(String[] args) {
List<Field> allFields = getAllFields(SomeClass.class);
System.out.println(allFields);
Map<String, List<Field>> reflectCache = buildReflectCache(SomeClass.class);
System.out.println(reflectCache);
}
}
</code>2. Combine reflection with Spring to locate annotated beans
Use Spring’s ApplicationContext to retrieve beans with a specific annotation:
Map<String, Object> applicationContext.getBeansWithAnnotation(Class)Define two annotations, @PrintInfoClass on a class and @PrintInfoMethod on a method, to print method signatures.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface PrintInfoClass {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface PrintInfoMethod {} @Component
public class PrintInfo implements ApplicationContextAware {
@PostConstruct
public void init() {
// Get beans annotated with @PrintInfoClass
Map<String, Object> beansWithAnnotationMap = applicationContext.getBeansWithAnnotation(PrintInfoClass.class);
for (Map.Entry<String, Object> entry : beansWithAnnotationMap.entrySet()) {
Object bean = entry.getValue();
for (Method method : bean.getMethods()) {
// Only process methods annotated with @PrintInfoMethod
if (method.isAnnotationPresent(PrintInfoMethod.class)) {
StringBuilder paramString = new StringBuilder();
Class<?>[] paramClassList = method.getParameterTypes();
for (int i = 0; i < paramClassList.length; ++i) {
Class<?> paramClass = paramClassList[i];
paramString.append(paramClass.getSimpleName());
if (i != paramClassList.length - 1) {
paramString.append(",");
}
}
System.out.print(Modifier.toString(method.getModifiers()) + " " +
method.getReturnType().getSimpleName() + " " +
method.getName() + "(" + paramString.toString() + ")
");
}
}
}
}
@Autowired
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
}Java Baker
Java architect and Raspberry Pi enthusiast, dedicated to writing high-quality technical articles; the same name is used across major platforms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
