Fundamentals 12 min read

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.

Java Baker
Java Baker
Java Baker
Unlock Java’s Power: Mastering Reflection for Dynamic Code

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;
    }
}
backendJavaprogrammingReflectionSpringdynamic
Java Baker
Written by

Java Baker

Java architect and Raspberry Pi enthusiast, dedicated to writing high-quality technical articles; the same name is used across major platforms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.