Dynamic Proxy Creation in Java

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

  1. Interface-Only: Can only proxy interfaces, not concrete classes
  2. Performance Overhead: Method invocation through reflection has some overhead
  3. Complex Debugging: Stack traces can be harder to read
  4. 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

  1. Cross-cutting concerns: Logging, security, transaction management
  2. Lazy initialization: Defer expensive object creation
  3. Remote method invocation: RPC, remoting
  4. Caching: Method-level caching
  5. Access control: Method-level security
  6. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper