← Back to list

Java Advanced Features in Action: Core Applications of Reflection and Dynamic Proxy in Spring

For non-members, please read for free here.

Umesh Kumar Yadav in Javarevisited · 2025-07-31 15:36 · 1 claps · 6.7 min read paywalled
#java #spring-boot #optimization #reflections #dynamic-proxy
Open on Medium ↗

Java Advanced Features in Action: Core Applications of Reflection and Dynamic Proxy in Spring

**For non-members, please read for free here.**

In Java development, reflection and dynamic proxies are often considered “advanced features.” While seemingly abstract, they underpin the core functionality of mainstream frameworks like Spring and MyBatis. This article, drawing on practical experience writing a Spring-like framework, explains how these two features solve practical problems, from the fundamentals to their implementation, and their irreplaceable role in framework design. 🚀

1. Reflection: Breaking Compile-Time Constraints

Reflection allows a program to access and dynamically manipulate a class’s structure (like its fields, methods, and annotations) at runtime. This capability shatters the traditional constraints of Java’s compile-time determinism and provides the foundation for a framework’s flexibility. In Spring, reflection is the core technology behind features like the IoC container and annotation parsing.

Core Capabilities and APIs

The heart of reflection is the java.lang.Class object, which represents the "metadata" of a class. Through it, you can obtain all information about the class.

Practical Application in Spring

IoC Container: Dynamic Bean Creation and Injection

The core of the IoC (Inversion of Control) container is that the container, not the developer, is responsible for creating beans and injecting dependencies. This entire process relies on reflection.

// Simplified BeanFactory implementation from a mini-Spring
public class SimpleBeanFactory {
    // Stores Bean definitions (class name, dependencies, etc.)
    private Map<String, BeanDefinition> beanDefinitions = new HashMap<>();

    // Get a Bean: The core is using reflection to create and inject
    public Object getBean(String beanName) throws Exception {
        BeanDefinition bd = beanDefinitions.get(beanName);
        Class<?> beanClass = Class.forName(bd.getClassName());
        // 1. Create an object via reflection (calling the no-arg constructor)
        Object bean = beanClass.getDeclaredConstructor().newInstance();
        // 2. Inject dependencies via reflection (processing @Autowired fields)
        for (Field field : beanClass.getDeclaredFields()) {
            if (field.isAnnotationPresent(Autowired.class)) {
                // Allow operations on private fields
                field.setAccessible(true);
                // Recursively get the dependent Bean (e.g., UserService needs UserDao)
                Object dependency = getBean(field.getName());
                // Inject the dependency
                field.set(bean, dependency);
            }
        }
        return bean;
    }
}

Core problems solved:

  • No need to hard-code new UserService(new UserDao()). The container dynamically creates objects and injects dependencies through reflection, achieving a "configuration-driven" model instead of a "code-driven" one.
  • Supports private field injection (via setAccessible(true)) without needing to expose setter methods, which helps maintain class encapsulation.

Annotation Parsing: Recognizing Annotations like @Transactional

The reason annotations like @Transactional and @RequestMapping work in Spring is that the framework scans for these annotations on classes and methods using reflection and then executes the corresponding logic.

// A simplified parser for the @Transactional annotation
public class TransactionAnnotationParser {
    public boolean isTransactional(Method method) {
        // Check if the method has @Transactional
        if (method.isAnnotationPresent(Transactional.class)) {
            return true;
        }
        // Check if the class has @Transactional (method annotation takes priority)
        return method.getDeclaringClass().isAnnotationPresent(Transactional.class);
    }

    // Get the propagation behavior configured in the annotation
    public Propagation getPropagation(Method method) {
        Transactional annotation = method.getAnnotation(Transactional.class);
        if (annotation == null) {
            annotation = method.getDeclaringClass().getAnnotation(Transactional.class);
        }
        return annotation.propagation();
    }
}

Core problems solved:

  • Annotations themselves are just “markers.” Reflection allows the framework to recognize these markers at runtime and trigger logic, like creating a transaction proxy for a @Transactional method.
  • Achieves “non-invasive” functional enhancements (like transactions and logs) without modifying the source code of the annotated class.

Performance and Tradeoffs of Reflection

Because reflection dynamically resolves a class’s structure, its performance is lower than direct calls (often 10–100 times slower). However, in framework design, this tradeoff is well worth it.

  • The core value of a framework is flexibility and development efficiency. The flexibility gained from reflection far outweighs the performance cost.
  • It can be optimized through caching. Caching reflected objects like Method and Field (as Spring's MethodCache does) avoids repeated parsing.

2. Dynamic Proxy: The Non-Invasive Enhancement Method

Dynamic proxies allow for the creation of proxy objects for target objects at runtime. This lets you insert enhanced logic (like logging and transactions) before and after the target method executes. It’s the technical foundation of AOP (Aspect-Oriented Programming). In Spring, dynamic proxies enable method interception and the reuse of cross-cutting concerns.

Two Types of Dynamic Proxies: JDK vs. CGLIB

Java has two mainstream implementations of dynamic proxies, and Spring automatically chooses one based on the target object’s type.

JDK Dynamic Proxy in Practice: AOP Method Interception

A JDK dynamic proxy is created using Proxy.newProxyInstance(). The core of the logic is defined in an InvocationHandler.

// 1. Target interface and implementation
public interface UserService {
    void saveUser(String username);
}

public class UserServiceImpl implements UserService {
    @Override
    public void saveUser(String username) {
        System.out.println("Saving user: " + username);
    }
}
// 2. Enhancement logic: a transaction interceptor (implements InvocationHandler)
public class TransactionInvocationHandler implements InvocationHandler {
    private final Object target; // The object being proxied
    public TransactionInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // Before advice: start transaction
        System.out.println("Opening transaction");
        try {
            // Execute the target method
            Object result = method.invoke(target, args);
            // After returning advice: commit transaction
            System.out.println("Committing transaction");
            return result;
        } catch (Exception e) {
            // After throwing advice: roll back transaction
            System.out.println("Rolling back transaction");
            throw e;
        }
    }
}
// 3. Proxy factory: creates the proxy object
public class ProxyFactory {
    public static Object createJdkProxy(Object target) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),       // ClassLoader
            target.getClass().getInterfaces(),        // Interfaces of the target object
            new TransactionInvocationHandler(target)  // Enhancement logic
        );
    }
}
// Example usage
public class Main {
    public static void main(String[] args) {
        UserService target = new UserServiceImpl();
        // Create proxy object (looks like UserService, but is actually a proxy)
        UserService proxy = (UserService) ProxyFactory.createJdkProxy(target);
        proxy.saveUser("Alice"); // Execution will trigger the transaction enhancement
    }
}

Execution results:

Opening transaction
Saving user: Alice
Committing transaction

CGLIB Proxy in Practice: Proxying Classes Without Interfaces

When a target object doesn’t implement an interface (like a plain POJO OrderService), you must use CGLIB.

// 1. Target class with no interface
public class OrderService {
    public void createOrder() {
        System.out.println("Creating order");
    }
}

// 2. Enhancement logic: CGLIB's MethodInterceptor
public class LogMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // Before advice: log start
        System.out.println("Method start: " + method.getName());
        // Execute target method (Note: CGLIB uses proxy.invokeSuper, not method.invoke)
        Object result = proxy.invokeSuper(obj, args);
        // After advice: log end
        System.out.println("Method end: " + method.getName());
        return result;
    }
}
// 3. Proxy factory: creates the CGLIB proxy
public class ProxyFactory {
    public static Object createCglibProxy(Class<?> targetClass) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetClass);      // Set superclass (the target)
        enhancer.setCallback(new LogMethodInterceptor()); // Set enhancer
        return enhancer.create();                 // Create proxy object
    }
}
// Example usage
public class Main {
    public static void main(String[] args) {
        OrderService proxy = (OrderService) ProxyFactory.createCglibProxy(OrderService.class);
        proxy.createOrder(); // Execution will trigger the logging enhancement
    }
}

Execution results:

Method start: createOrder
Creating order
Method end: createOrder

The Core Value of Dynamic Proxy in AOP

The essence of AOP is separating cross-cutting concerns (like transactions and logging) from business logic. Dynamic proxies are key to achieving this.

  • Non-invasive: Business classes (UserServiceImpl) don't need to be modified. Enhanced logic is applied via proxy objects.
  • Reusability: Cross-cutting logic (like transaction management) is written once and applied to multiple targets via proxies.
  • Flexibility: You can dynamically choose whether to apply an enhancement (e.g., add logging in dev but not in prod) and even switch the enhancement logic dynamically.

Proxy Selection Strategy in Spring

Spring’s ProxyFactory will automatically select the proxy method based on the target object's type. The logic is as follows:

public class ProxyFactory {
    public static Object createProxy(Object target) {
        // If the target class implements interfaces, use JDK proxy
        if (target.getClass().getInterfaces().length > 0) {
            return createJdkProxy(target);
        } else {
            // Otherwise, use CGLIB proxy
            return createCglibProxy(target.getClass());
        }
    }
}

Why is it designed this way?

  • JDK proxy is natively supported by the JDK, requires no extra dependencies, and has slightly better performance than CGLIB for interface method calls.
  • CGLIB can proxy classes without interfaces, compensating for the limitations of the JDK proxy. However, it requires a third-party library and cannot proxy final classes or methods (because it's based on inheritance).

3. Reflection and Dynamic Proxy: The Golden Partnership 🤝

Reflection and dynamic proxies are not isolated; they often work together in frameworks. Taking Spring AOP as an example:

  • Reflection Scanning: The framework scans all classes via reflection, identifies aspect classes annotated with @Aspect, and parses the advice and pointcuts from annotations like @Before and @After (e.g., execution(* save*(..))).
  • Dynamic Proxy Creation: For a target class that matches a pointcut, a proxy object is created using either JDK or CGLIB.
  • Reflection Invocation: When the proxy object is executed, it can use reflection to get annotations from the target method (like @Transactional) and execute enhanced logic (like transaction control) based on the annotation's configuration.

This combination allows a framework to both perceive code structure (reflection) and enhance code behavior (dynamic proxy), ultimately realizing Spring’s core design philosophy of being “non-invasive.”

IV. Summary: From “How” to “Why”

Reflection and dynamic proxies are called “advanced features” not just because their APIs are complex, but because they embody Java’s “dynamic” philosophy — breaking free from compile-time constraints to give programs greater flexibility at runtime.

In actual development:

  • For business code, use reflection and dynamic proxies with caution. They can reduce readability, and the performance cost can be significant in high-frequency scenarios.
  • For frameworks or common components (like utility classes and middleware), they are powerful tools for achieving “low coupling and high scalability” and are worth mastering in depth.

Thank you for your patience in reading this article!

If you found this article helpful, please give it a clap 👏, and share it with friends in need and follow for more Spring Boot insights.

Your support is my biggest motivation to continue to output technical insights!


메타데이터
post_id
f2e1185eb7d3
slug
java-advanced-features-in-action-core-applications-of-reflection-and-dynamic-proxy-in-spring-f2e1185eb7d3
url
https://medium.com/javarevisited/java-advanced-features-in-action-core-applications-of-reflection-and-dynamic-proxy-in-spring-f2e1185eb7d3
canonical_url
https://medium.com/javarevisited/java-advanced-features-in-action-core-applications-of-reflection-and-dynamic-proxy-in-spring-f2e1185eb7d3
author_url
https://medium.com/@umeshcapg
status
ok
fetched_at
2026-07-11 12:15:36