Introduction to Dynamic Proxies
Dynamic proxies in Java provide a mechanism to create proxy classes and instances at runtime. They allow you to intercept method invocations and add cross-cutting concerns like logging, security, or transaction management without modifying the original class code.
Key Concepts
- Proxy Pattern: Structural pattern that provides a surrogate or placeholder for another object
- Invocation Handler: Intercepts method calls and adds custom behavior
- Runtime Generation: Proxy classes are generated dynamically at runtime
1. Core Components
java.lang.reflect.Proxy Class
The main class for creating dynamic proxies. Key methods:
// Primary method for creating dynamic proxies static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) // Other utility methods static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) static boolean isProxyClass(Class<?> cl) static InvocationHandler getInvocationHandler(Object proxy)
java.lang.reflect.InvocationHandler Interface
Single method interface for handling method invocations:
public interface InvocationHandler { Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } 2. Basic Dynamic Proxy Example
Simple Logging Proxy
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; // Interface we want to proxy interface UserService { void createUser(String username, String email); void deleteUser(String username); String getUserInfo(String username); } // Concrete implementation class UserServiceImpl implements UserService { @Override public void createUser(String username, String email) { System.out.println("Creating user: " + username); // Actual implementation... } @Override public void deleteUser(String username) { System.out.println("Deleting user: " + username); // Actual implementation... } @Override public String getUserInfo(String username) { return "User info for: " + username; } } // Logging Invocation Handler class LoggingHandler implements InvocationHandler { private final Object target; public LoggingHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Before method execution System.out.println(">>> Calling method: " + method.getName()); if (args != null) { System.out.println(">>> Arguments: " + Arrays.toString(args)); } long startTime = System.nanoTime(); try { // Execute the original method Object result = method.invoke(target, args); // After method execution System.out.println(">>> Method " + method.getName() + " completed successfully"); if (result != null) { System.out.println(">>> Return value: " + result); } return result; } catch (Exception e) { // Exception handling System.out.println(">>> Method " + method.getName() + " failed with exception: " + e.getMessage()); throw e; } finally { // Performance monitoring long duration = System.nanoTime() - startTime; System.out.println(">>> Execution time: " + duration + " ns"); } } } public class BasicProxyExample { public static void main(String[] args) { // Create target object UserService realService = new UserServiceImpl(); // Create proxy instance UserService proxyService = (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class<?>[] { UserService.class }, new LoggingHandler(realService) ); // Use the proxy proxyService.createUser("john_doe", "[email protected]"); System.out.println(); String info = proxyService.getUserInfo("john_doe"); System.out.println(); proxyService.deleteUser("john_doe"); } } Output:
>>> Calling method: createUser >>> Arguments: [john_doe, [email protected]] Creating user: john_doe >>> Method createUser completed successfully >>> Execution time: 123456 ns >>> Calling method: getUserInfo >>> Arguments: [john_doe] >>> Method getUserInfo completed successfully >>> Return value: User info for: john_doe >>> Execution time: 45678 ns >>> Calling method: deleteUser >>> Arguments: [john_doe] Deleting user: john_doe >>> Method deleteUser completed successfully >>> Execution time: 78901 ns
3. Advanced Proxy Patterns
Caching Proxy
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; interface DataService { String fetchData(String key); void clearCache(); } class DataServiceImpl implements DataService { @Override public String fetchData(String key) { // Simulate expensive operation try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "Data for: " + key + " (expensive operation)"; } @Override public void clearCache() { // Actual implementation } } class CachingHandler implements InvocationHandler { private final Object target; private final Map<String, Object> cache = new HashMap<>(); public CachingHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // Handle cacheable methods if ("fetchData".equals(methodName) && args.length == 1) { String key = (String) args[0]; String cacheKey = methodName + ":" + key; // Check cache first if (cache.containsKey(cacheKey)) { System.out.println("Cache HIT for key: " + key); return cache.get(cacheKey); } System.out.println("Cache MISS for key: " + key); Object result = method.invoke(target, args); cache.put(cacheKey, result); return result; } // Handle cache clearing if ("clearCache".equals(methodName)) { cache.clear(); System.out.println("Cache cleared"); return method.invoke(target, args); } // Default behavior for other methods return method.invoke(target, args); } } public class CachingProxyExample { public static void main(String[] args) throws Exception { DataService realService = new DataServiceImpl(); DataService cachedService = (DataService) Proxy.newProxyInstance( DataService.class.getClassLoader(), new Class<?>[] { DataService.class }, new CachingHandler(realService) ); // First call - cache miss long start = System.currentTimeMillis(); String result1 = cachedService.fetchData("key1"); long time1 = System.currentTimeMillis() - start; System.out.println("Result: " + result1 + " (took " + time1 + "ms)"); // Second call - cache hit start = System.currentTimeMillis(); String result2 = cachedService.fetchData("key1"); long time2 = System.currentTimeMillis() - start; System.out.println("Result: " + result2 + " (took " + time2 + "ms)"); // Clear cache cachedService.clearCache(); // Third call - cache miss again start = System.currentTimeMillis(); String result3 = cachedService.fetchData("key1"); long time3 = System.currentTimeMillis() - start; System.out.println("Result: " + result3 + " (took " + time3 + "ms)"); } } Security Proxy
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.HashSet; import java.util.Set; interface AdminService { void deleteUser(String username); void updateConfig(String config); String viewStats(); } class AdminServiceImpl implements AdminService { @Override public void deleteUser(String username) { System.out.println("Deleting user: " + username); } @Override public void updateConfig(String config) { System.out.println("Updating config: " + config); } @Override public String viewStats() { return "System statistics..."; } } class SecurityHandler implements InvocationHandler { private final Object target; private final Set<String> currentUserRoles; public SecurityHandler(Object target, Set<String> userRoles) { this.target = target; this.currentUserRoles = new HashSet<>(userRoles); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // Check permissions based on method if (requiresAdmin(methodName) && !currentUserRoles.contains("ADMIN")) { throw new SecurityException("Access denied. ADMIN role required for: " + methodName); } if (requiresOperator(methodName) && !currentUserRoles.contains("OPERATOR") && !currentUserRoles.contains("ADMIN")) { throw new SecurityException("Access denied. OPERATOR or ADMIN role required for: " + methodName); } System.out.println("Access granted for: " + methodName); return method.invoke(target, args); } private boolean requiresAdmin(String methodName) { return methodName.startsWith("delete") || methodName.startsWith("update"); } private boolean requiresOperator(String methodName) { return methodName.startsWith("view"); } } public class SecurityProxyExample { public static void main(String[] args) { AdminService realService = new AdminServiceImpl(); // Create proxy with user roles Set<String> userRoles = new HashSet<>(Arrays.asList("USER")); // No admin rights AdminService securedService = (AdminService) Proxy.newProxyInstance( AdminService.class.getClassLoader(), new Class<?>[] { AdminService.class }, new SecurityHandler(realService, userRoles) ); try { // This should fail - user doesn't have ADMIN role securedService.deleteUser("someuser"); } catch (Exception e) { System.out.println("Expected error: " + e.getMessage()); } try { // This should also fail securedService.updateConfig("new config"); } catch (Exception e) { System.out.println("Expected error: " + e.getMessage()); } // This might work depending on security rules try { String stats = securedService.viewStats(); System.out.println("Stats: " + stats); } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } 4. Multiple Interface Proxying
Proxying Multiple Interfaces
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; interface Logger { void log(String message); void error(String message); } interface Configurable { void setConfiguration(String config); String getConfiguration(); } class MultiService implements Logger, Configurable { private String configuration = "default"; @Override public void log(String message) { System.out.println("LOG: " + message); } @Override public void error(String message) { System.err.println("ERROR: " + message); } @Override public void setConfiguration(String config) { this.configuration = config; } @Override public String getConfiguration() { return configuration; } } class MultiInterfaceHandler implements InvocationHandler { private final Object target; public MultiInterfaceHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Intercepting: " + method.getDeclaringClass().getSimpleName() + "." + method.getName()); // Add cross-cutting concerns here if (method.getName().startsWith("set")) { System.out.println("Modifying state..."); } return method.invoke(target, args); } } public class MultiInterfaceProxyExample { public static void main(String[] args) { MultiService realService = new MultiService(); // Create proxy implementing multiple interfaces Object proxy = Proxy.newProxyInstance( MultiService.class.getClassLoader(), new Class<?>[] { Logger.class, Configurable.class }, new MultiInterfaceHandler(realService) ); // Use as Logger Logger logger = (Logger) proxy; logger.log("This is a log message"); logger.error("This is an error"); // Use as Configurable Configurable configurable = (Configurable) proxy; configurable.setConfiguration("new config"); System.out.println("Config: " + configurable.getConfiguration()); // Check if it's a proxy System.out.println("Is proxy: " + Proxy.isProxyClass(proxy.getClass())); System.out.println("Proxy interfaces: " + Arrays.toString(proxy.getClass().getInterfaces())); } } 5. Advanced Patterns and Techniques
Lazy Initialization Proxy
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; interface ExpensiveService { void performOperation(); void heavyInitialization(); } class ExpensiveServiceImpl implements ExpensiveService { public ExpensiveServiceImpl() { System.out.println("Creating expensive service..."); // Simulate heavy initialization try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Expensive service created!"); } @Override public void performOperation() { System.out.println("Performing operation..."); } @Override public void heavyInitialization() { System.out.println("Heavy initialization completed"); } } class LazyInitializationHandler implements InvocationHandler { private final Class<?> serviceClass; private Object target; public LazyInitializationHandler(Class<?> serviceClass) { this.serviceClass = serviceClass; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Lazy initialization on first method call if (target == null) { System.out.println("Initializing service lazily..."); target = serviceClass.getDeclaredConstructor().newInstance(); } return method.invoke(target, args); } } public class LazyInitializationProxy { public static void main(String[] args) { System.out.println("Application starting..."); // Proxy created immediately, but real service is created lazily ExpensiveService service = (ExpensiveService) Proxy.newProxyInstance( ExpensiveService.class.getClassLoader(), new Class<?>[] { ExpensiveService.class }, new LazyInitializationHandler(ExpensiveServiceImpl.class) ); System.out.println("Proxy created, but real service not initialized yet"); // First method call triggers initialization service.performOperation(); } } Method Filtering Proxy
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Set; import java.util.HashSet; import java.util.Arrays; interface Calculator { int add(int a, int b); int subtract(int a, int b); int multiply(int a, int b); int divide(int a, int b); } class CalculatorImpl implements Calculator { @Override public int add(int a, int b) { return a + b; } @Override public int subtract(int a, int b) { return a - b; } @Override public int multiply(int a, int b) { return a * b; } @Override public int divide(int a, int b) { if (b == 0) throw new ArithmeticException("Division by zero"); return a / b; } } class MethodFilteringHandler implements InvocationHandler { private final Object target; private final Set<String> allowedMethods; public MethodFilteringHandler(Object target, Set<String> allowedMethods) { this.target = target; this.allowedMethods = allowedMethods; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (!allowedMethods.contains(method.getName())) { throw new UnsupportedOperationException( "Method " + method.getName() + " is not allowed"); } System.out.println("Executing allowed method: " + method.getName()); return method.invoke(target, args); } } public class MethodFilteringProxy { public static void main(String[] args) { Calculator realCalculator = new CalculatorImpl(); // Only allow add and subtract methods Set<String> allowedMethods = new HashSet<>(Arrays.asList("add", "subtract")); Calculator filteredCalculator = (Calculator) Proxy.newProxyInstance( Calculator.class.getClassLoader(), new Class<?>[] { Calculator.class }, new MethodFilteringHandler(realCalculator, allowedMethods) ); // These will work System.out.println("5 + 3 = " + filteredCalculator.add(5, 3)); System.out.println("5 - 3 = " + filteredCalculator.subtract(5, 3)); // These will throw UnsupportedOperationException try { filteredCalculator.multiply(5, 3); } catch (Exception e) { System.out.println("Expected: " + e.getMessage()); } try { filteredCalculator.divide(6, 2); } catch (Exception e) { System.out.println("Expected: " + e.getMessage()); } } } 6. Utility Methods and Reflection
Proxy Utility Class
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; public class ProxyUtils { // Cache for proxy classes to improve performance private static final ConcurrentMap<ClassLoader, ConcurrentMap<String, Class<?>>> proxyClassCache = new ConcurrentHashMap<>(); /** * Creates a synchronized proxy that makes all methods thread-safe */ @SuppressWarnings("unchecked") public static <T> T createSynchronizedProxy(T target, Class<T> interfaceType) { return (T) Proxy.newProxyInstance( interfaceType.getClassLoader(), new Class<?>[] { interfaceType }, new SynchronizedHandler(target) ); } /** * Creates a timing proxy that measures method execution time */ @SuppressWarnings("unchecked") public static <T> T createTimingProxy(T target, Class<T> interfaceType) { return (T) Proxy.newProxyInstance( interfaceType.getClassLoader(), new Class<?>[] { interfaceType }, new TimingHandler(target) ); } /** * Creates a proxy that retries failed operations */ @SuppressWarnings("unchecked") public static <T> T createRetryProxy(T target, Class<T> interfaceType, int maxRetries) { return (T) Proxy.newProxyInstance( interfaceType.getClassLoader(), new Class<?>[] { interfaceType }, new RetryHandler(target, maxRetries) ); } // Synchronized handler private static class SynchronizedHandler implements InvocationHandler { private final Object target; private final Object lock = new Object(); public SynchronizedHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { synchronized (lock) { return method.invoke(target, args); } } } // Timing handler private static class TimingHandler implements InvocationHandler { private final Object target; public TimingHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start = System.nanoTime(); try { return method.invoke(target, args); } finally { long duration = System.nanoTime() - start; System.out.printf("Method %s took %.3f ms%n", method.getName(), duration / 1_000_000.0); } } } // Retry handler private static class RetryHandler implements InvocationHandler { private final Object target; private final int maxRetries; public RetryHandler(Object target, int maxRetries) { this.target = target; this.maxRetries = maxRetries; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { int attempts = 0; while (true) { try { return method.invoke(target, args); } catch (Exception e) { attempts++; if (attempts > maxRetries) { throw e; } System.out.printf("Attempt %d failed for %s, retrying...%n", attempts, method.getName()); Thread.sleep(100 * attempts); // Exponential backoff } } } } } // Usage example interface DataProcessor { String process(String input); } class DataProcessorImpl implements DataProcessor { @Override public String process(String input) { // Simulate occasional failure if (Math.random() < 0.3) { throw new RuntimeException("Processing failed"); } return "Processed: " + input; } } public class UtilityExample { public static void main(String[] args) { DataProcessor processor = new DataProcessorImpl(); // Create retry proxy DataProcessor retryProcessor = ProxyUtils.createRetryProxy( processor, DataProcessor.class, 3); // Use the proxy for (int i = 0; i < 5; i++) { try { String result = retryProcessor.process("data " + i); System.out.println("Success: " + result); } catch (Exception e) { System.out.println("Final failure: " + e.getMessage()); } } } } 7. Limitations and Considerations
Limitations of Dynamic Proxies
- Interface-Only: Can only proxy interfaces, not concrete classes
- Performance Overhead: Method invocation through reflection has some overhead
- Complex Debugging: Stack traces can be harder to read
- equals/hashCode/toString: These methods on the proxy itself, not the target
Workaround for equals/hashCode/toString
class SmartInvocationHandler implements InvocationHandler { private final Object target; public SmartInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // Handle Object methods specially if ("equals".equals(methodName) && args != null && args.length == 1) { Object arg = args[0]; if (arg == null) return false; if (proxy == arg) return true; if (!Proxy.isProxyClass(arg.getClass())) return false; InvocationHandler otherHandler = Proxy.getInvocationHandler(arg); return otherHandler instanceof SmartInvocationHandler && target.equals(((SmartInvocationHandler) otherHandler).target); } if ("hashCode".equals(methodName)) { return target.hashCode(); } if ("toString".equals(methodName)) { return "Proxy[" + target.toString() + "]"; } // Default behavior for other methods return method.invoke(target, args); } } 8. Best Practices
When to Use Dynamic Proxies
- Cross-cutting concerns: Logging, security, transaction management
- Lazy initialization: Defer expensive object creation
- Remote method invocation: RPC, remoting
- Caching: Method-level caching
- Access control: Method-level security
- Testing: Mock objects, test doubles
Performance Considerations
// Cache Method objects for better performance class OptimizedHandler implements InvocationHandler { private final Object target; private final ConcurrentMap<String, Method> methodCache = new ConcurrentHashMap<>(); public OptimizedHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Cache method lookup String methodKey = method.getName() + Arrays.toString(method.getParameterTypes()); Method cachedMethod = methodCache.computeIfAbsent(methodKey, k -> { try { return target.getClass().getMethod(method.getName(), method.getParameterTypes()); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } }); // Use cached method for invocation return cachedMethod.invoke(target, args); } } Error Handling
class RobustInvocationHandler implements InvocationHandler { private final Object target; public RobustInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // Pre-processing validateArguments(method, args); // Method execution Object result = method.invoke(target, args); // Post-processing validateResult(method, result); return result; } catch (IllegalAccessException e) { throw new RuntimeException("Access denied for method: " + method.getName(), e); } catch (IllegalArgumentException e) { throw new RuntimeException("Invalid arguments for method: " + method.getName(), e); } catch (Exception e) { // Unwrap InvocationTargetException if (e.getCause() != null) { throw e.getCause(); } throw e; } } private void validateArguments(Method method, Object[] args) { // Add argument validation logic } private void validateResult(Method method, Object result) { // Add result validation logic } } Summary
Dynamic proxies in Java provide a powerful mechanism for:
- Aspect-oriented programming without modifying original code
- Runtime behavior modification through interception
- Cross-cutting concerns implementation
- Flexible object composition
Key Benefits:
- Clean separation of concerns
- Runtime flexibility
- Reduced code duplication
- Enhanced testability
Common Use Cases:
- Spring Framework AOP
- Hibernate lazy loading
- RMI and remoting
- Transaction management
- Security enforcement
- Logging and monitoring
Dynamic proxies are an essential tool in advanced Java development, enabling clean, maintainable architectures with powerful runtime capabilities.