Dynamic Proxies in Java

Dynamic proxies provide a mechanism for creating proxy instances for interfaces at runtime. They are powerful tools for implementing cross-cutting concerns like logging, security, transaction management, and more.

1. Basic Dynamic Proxy

Simple InvocationHandler Implementation

import java.lang.reflect.*;
public class BasicDynamicProxy {
// Interface we want to proxy
interface UserService {
String getUser(String userId);
void saveUser(String userId, String userData);
void deleteUser(String userId);
}
// Real implementation
static class UserServiceImpl implements UserService {
@Override
public String getUser(String userId) {
System.out.println("Getting user: " + userId);
return "User data for: " + userId;
}
@Override
public void saveUser(String userId, String userData) {
System.out.println("Saving user: " + userId + " with data: " + userData);
}
@Override
public void deleteUser(String userId) {
System.out.println("Deleting user: " + userId);
}
}
// Dynamic proxy handler
static 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 {
System.out.println(">>> Before method: " + method.getName());
if (args != null) {
System.out.println("    Arguments: " + java.util.Arrays.toString(args));
}
long startTime = System.nanoTime();
Object result = method.invoke(target, args);
long endTime = System.nanoTime();
System.out.println("    Result: " + result);
System.out.println("    Execution time: " + (endTime - startTime) + " ns");
System.out.println("<<< After method: " + method.getName());
return result;
}
}
public static void main(String[] args) {
// Create real object
UserService realService = new UserServiceImpl();
// Create proxy instance
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[] { UserService.class },
new LoggingHandler(realService)
);
// Use proxy
proxy.getUser("123");
System.out.println();
proxy.saveUser("456", "John Doe");
System.out.println();
proxy.deleteUser("789");
}
}

2. Advanced Dynamic Proxy Patterns

Generic Proxy Factory

import java.lang.reflect.*;
import java.util.*;
public class ProxyFactory {
@SuppressWarnings("unchecked")
public static <T> T createProxy(T target, Class<T> interfaceType, InvocationHandler... handlers) {
return (T) Proxy.newProxyInstance(
interfaceType.getClassLoader(),
new Class<?>[] { interfaceType },
new ChainedInvocationHandler(target, handlers)
);
}
// Chain multiple handlers
static class ChainedInvocationHandler implements InvocationHandler {
private final Object target;
private final List<InvocationHandler> handlers;
public ChainedInvocationHandler(Object target, InvocationHandler... handlers) {
this.target = target;
this.handlers = new ArrayList<>(Arrays.asList(handlers));
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Create invocation context
InvocationContext context = new InvocationContext(proxy, method, args, target);
// Execute handlers in chain
for (InvocationHandler handler : handlers) {
handler.invoke(proxy, method, args);
}
// Finally invoke the target method
return method.invoke(target, args);
}
}
static class InvocationContext {
public final Object proxy;
public final Method method;
public final Object[] args;
public final Object target;
public Object result;
public Throwable exception;
public InvocationContext(Object proxy, Method method, Object[] args, Object target) {
this.proxy = proxy;
this.method = method;
this.args = args;
this.target = target;
}
}
}
// Example usage with multiple handlers
class AdvancedProxyExample {
interface Calculator {
int add(int a, int b);
int multiply(int a, int b);
double divide(double a, double b);
}
static class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int multiply(int a, int b) {
return a * b;
}
@Override
public double divide(double a, double b) {
if (b == 0) throw new IllegalArgumentException("Division by zero");
return a / b;
}
}
// Logging handler
static class LoggingHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.printf("[LOG] Calling %s with args: %s%n", 
method.getName(), Arrays.toString(args));
return null; // Continue chain
}
}
// Timing handler
static class TimingHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
try {
return null; // Continue chain
} finally {
long duration = System.currentTimeMillis() - start;
System.out.printf("[TIMING] %s took %d ms%n", method.getName(), duration);
}
}
}
// Security handler
static class SecurityHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("divide")) {
System.out.println("[SECURITY] Checking division operation...");
}
return null; // Continue chain
}
}
public static void main(String[] args) {
Calculator realCalculator = new CalculatorImpl();
Calculator proxy = ProxyFactory.createProxy(
realCalculator,
Calculator.class,
new LoggingHandler(),
new TimingHandler(),
new SecurityHandler()
);
System.out.println("Result: " + proxy.add(5, 3));
System.out.println();
System.out.println("Result: " + proxy.multiply(4, 7));
System.out.println();
try {
System.out.println("Result: " + proxy.divide(10.0, 2.0));
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
}

3. Real-World Use Cases

Database Connection Pool with Proxy

import java.lang.reflect.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
public class ConnectionPoolProxy {
interface ConnectionPool {
Connection getConnection() throws SQLException;
void releaseConnection(Connection connection);
int getActiveConnections();
int getIdleConnections();
}
static class SimpleConnectionPool implements ConnectionPool {
private final BlockingQueue<Connection> pool;
private final String url;
private final String username;
private final String password;
private final int maxConnections;
private final AtomicInteger activeConnections = new AtomicInteger(0);
public SimpleConnectionPool(String url, String username, String password, int maxConnections) {
this.url = url;
this.username = username;
this.password = password;
this.maxConnections = maxConnections;
this.pool = new LinkedBlockingQueue<>(maxConnections);
}
@Override
public Connection getConnection() throws SQLException {
Connection conn = pool.poll();
if (conn != null) {
activeConnections.incrementAndGet();
return conn;
}
if (activeConnections.get() < maxConnections) {
Connection newConn = DriverManager.getConnection(url, username, password);
activeConnections.incrementAndGet();
return newConn;
}
throw new SQLException("Connection pool exhausted");
}
@Override
public void releaseConnection(Connection connection) {
if (connection != null) {
pool.offer(connection);
activeConnections.decrementAndGet();
}
}
@Override
public int getActiveConnections() {
return activeConnections.get();
}
@Override
public int getIdleConnections() {
return pool.size();
}
}
// Proxy that tracks connection usage
static class TrackingConnectionProxy implements InvocationHandler {
private final Connection realConnection;
private final ConnectionPool pool;
private boolean closed = false;
private long lastUsedTime;
public TrackingConnectionProxy(Connection realConnection, ConnectionPool pool) {
this.realConnection = realConnection;
this.pool = pool;
this.lastUsedTime = System.currentTimeMillis();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Update last used time
lastUsedTime = System.currentTimeMillis();
// Handle close method specially
if ("close".equals(method.getName())) {
if (!closed) {
closed = true;
pool.releaseConnection((Connection) proxy);
System.out.println("Connection returned to pool");
}
return null;
}
// Check if connection is closed
if (closed) {
throw new SQLException("Connection is closed");
}
// Delegate to real connection
return method.invoke(realConnection, args);
}
public long getIdleTime() {
return System.currentTimeMillis() - lastUsedTime;
}
}
// Enhanced connection pool with proxy
static class ProxiedConnectionPool implements ConnectionPool {
private final SimpleConnectionPool realPool;
public ProxiedConnectionPool(String url, String username, String password, int maxConnections) {
this.realPool = new SimpleConnectionPool(url, username, password, maxConnections);
}
@Override
public Connection getConnection() throws SQLException {
Connection realConn = realPool.getConnection();
// Create proxy for the connection
return (Connection) Proxy.newProxyInstance(
Connection.class.getClassLoader(),
new Class<?>[] { Connection.class },
new TrackingConnectionProxy(realConn, realPool)
);
}
@Override
public void releaseConnection(Connection connection) {
// Not needed - handled by proxy
}
@Override
public int getActiveConnections() {
return realPool.getActiveConnections();
}
@Override
public int getIdleConnections() {
return realPool.getIdleConnections();
}
}
public static void main(String[] args) throws Exception {
// Simulate database connection pool with proxies
ProxiedConnectionPool pool = new ProxiedConnectionPool(
"jdbc:mysql://localhost:3306/test", "user", "pass", 5
);
// Get connections through proxy
Connection conn1 = pool.getConnection();
System.out.println("Got connection 1");
// Use connection
Statement stmt = conn1.createStatement();
// Close connection (returns to pool via proxy)
conn1.close();
System.out.println("Active connections: " + pool.getActiveConnections());
System.out.println("Idle connections: " + pool.getIdleConnections());
}
}

Caching Proxy

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
public class CachingProxyExample {
interface DataService {
String fetchData(String key);
void updateData(String key, String value);
void clearData(String key);
}
static class ExpensiveDataService implements DataService {
@Override
public String fetchData(String key) {
// Simulate expensive operation (database call, API call, etc.)
System.out.println(">>> Performing expensive fetch for: " + key);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Data for " + key + " at " + System.currentTimeMillis();
}
@Override
public void updateData(String key, String value) {
System.out.println("Updating data for: " + key);
// Simulate update operation
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void clearData(String key) {
System.out.println("Clearing data for: " + key);
}
}
static class CachingHandler implements InvocationHandler {
private final Object target;
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
private final long defaultTtl; // Time to live in milliseconds
public CachingHandler(Object target, long defaultTtl) {
this.target = target;
this.defaultTtl = defaultTtl;
}
static class CacheEntry {
final Object value;
final long expiryTime;
CacheEntry(Object value, long ttl) {
this.value = value;
this.expiryTime = System.currentTimeMillis() + ttl;
}
boolean isExpired() {
return System.currentTimeMillis() > expiryTime;
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if ("fetchData".equals(methodName)) {
return handleFetchData(method, args);
} else if ("updateData".equals(methodName) || "clearData".equals(methodName)) {
return handleDataModification(method, args);
} else {
return method.invoke(target, args);
}
}
private Object handleFetchData(Method method, Object[] args) throws Throwable {
String key = (String) args[0];
CacheEntry entry = cache.get(key);
// Return cached value if present and not expired
if (entry != null && !entry.isExpired()) {
System.out.println(">>> Cache HIT for: " + key);
return entry.value;
}
// Cache miss or expired - fetch from target
System.out.println(">>> Cache MISS for: " + key);
Object result = method.invoke(target, args);
// Cache the result
cache.put(key, new CacheEntry(result, defaultTtl));
return result;
}
private Object handleDataModification(Method method, Object[] args) throws Throwable {
// Invalidate cache for modified keys
if (args.length > 0) {
String key = (String) args[0];
cache.remove(key);
System.out.println(">>> Cache invalidated for: " + key);
}
return method.invoke(target, args);
}
public void clearCache() {
cache.clear();
System.out.println(">>> Cache cleared");
}
public int getCacheSize() {
return cache.size();
}
}
public static void main(String[] args) throws Exception {
DataService realService = new ExpensiveDataService();
DataService cachedService = (DataService) Proxy.newProxyInstance(
DataService.class.getClassLoader(),
new Class<?>[] { DataService.class },
new CachingHandler(realService, 5000) // 5 second TTL
);
// First call - cache miss
System.out.println("First call:");
String result1 = cachedService.fetchData("key1");
System.out.println("Result: " + result1);
System.out.println();
// Second call - cache hit
System.out.println("Second call (within TTL):");
String result2 = cachedService.fetchData("key1");
System.out.println("Result: " + result2);
System.out.println();
// Update data - invalidates cache
System.out.println("Updating data:");
cachedService.updateData("key1", "new value");
System.out.println();
// Third call - cache miss after update
System.out.println("Third call (after update):");
String result3 = cachedService.fetchData("key1");
System.out.println("Result: " + result3);
}
}

4. Security and Access Control Proxy

Role-Based Access Control Proxy

import java.lang.reflect.*;
import java.util.*;
public class SecurityProxyExample {
interface SecureService {
@RequiresRole("ADMIN")
void deleteUser(String userId);
@RequiresRole("USER")
String viewProfile(String userId);
@RequiresRole({"ADMIN", "MANAGER"})
void updateUser(String userId, String userData);
String publicInfo(); // No role required
}
@interface RequiresRole {
String[] value();
}
static class SecureServiceImpl implements SecureService {
@Override
public void deleteUser(String userId) {
System.out.println("Deleting user: " + userId);
}
@Override
public String viewProfile(String userId) {
return "Profile data for: " + userId;
}
@Override
public void updateUser(String userId, String userData) {
System.out.println("Updating user: " + userId + " with data: " + userData);
}
@Override
public String publicInfo() {
return "Public information";
}
}
static class SecurityHandler implements InvocationHandler {
private final Object target;
private final Set<String> userRoles;
public SecurityHandler(Object target, Set<String> userRoles) {
this.target = target;
this.userRoles = userRoles;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Check for @RequiresRole annotation
RequiresRole annotation = method.getAnnotation(RequiresRole.class);
if (annotation != null) {
String[] requiredRoles = annotation.value();
if (!hasRequiredRole(requiredRoles)) {
throw new SecurityException(
"Access denied. User lacks required roles: " + 
Arrays.toString(requiredRoles) + 
" for method: " + method.getName()
);
}
}
// User has required role or no role required
return method.invoke(target, args);
}
private boolean hasRequiredRole(String[] requiredRoles) {
for (String requiredRole : requiredRoles) {
if (userRoles.contains(requiredRole)) {
return true;
}
}
return false;
}
}
public static void main(String[] args) {
SecureService realService = new SecureServiceImpl();
// Test with different user roles
testWithRoles(realService, new HashSet<>(Arrays.asList("USER")));        // Regular user
testWithRoles(realService, new HashSet<>(Arrays.asList("MANAGER")));     // Manager
testWithRoles(realService, new HashSet<>(Arrays.asList("ADMIN")));       // Admin
testWithRoles(realService, new HashSet<>(Arrays.asList("USER", "MANAGER"))); // Multiple roles
}
private static void testWithRoles(SecureService realService, Set<String> roles) {
System.out.println("\n=== Testing with roles: " + roles + " ===");
SecureService securedService = (SecureService) Proxy.newProxyInstance(
SecureService.class.getClassLoader(),
new Class<?>[] { SecureService.class },
new SecurityHandler(realService, roles)
);
try {
// Public method - should always work
System.out.println("Public info: " + securedService.publicInfo());
// Role-specific methods
System.out.println("View profile: " + securedService.viewProfile("123"));
securedService.updateUser("123", "new data");
securedService.deleteUser("123");
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
}

5. Transaction Management Proxy

Transactional Method Interceptor

import java.lang.reflect.*;
import java.sql.*;
import java.util.*;
public class TransactionProxyExample {
interface UserRepository {
@Transactional
void saveUser(User user) throws SQLException;
@Transactional
void updateUser(User user) throws SQLException;
User findUserById(int id) throws SQLException; // Not transactional
@Transactional
void transferPoints(int fromUserId, int toUserId, int points) throws SQLException;
}
@interface Transactional {
// Marker annotation for transactional methods
}
static class User {
private int id;
private String name;
private int points;
public User(int id, String name, int points) {
this.id = id;
this.name = name;
this.points = points;
}
// Getters and setters
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getPoints() { return points; }
public void setPoints(int points) { this.points = points; }
@Override
public String toString() {
return String.format("User{id=%d, name='%s', points=%d}", id, name, points);
}
}
static class UserRepositoryImpl implements UserRepository {
private Connection connection;
public UserRepositoryImpl(Connection connection) {
this.connection = connection;
}
@Override
@Transactional
public void saveUser(User user) throws SQLException {
System.out.println("Saving user: " + user);
// Simulate database operation
if (user.getName().contains("error")) {
throw new SQLException("Simulated database error");
}
}
@Override
@Transactional
public void updateUser(User user) throws SQLException {
System.out.println("Updating user: " + user);
}
@Override
public User findUserById(int id) throws SQLException {
System.out.println("Finding user: " + id);
return new User(id, "User" + id, 100);
}
@Override
@Transactional
public void transferPoints(int fromUserId, int toUserId, int points) throws SQLException {
System.out.printf("Transferring %d points from user %d to user %d%n", 
points, fromUserId, toUserId);
// Simulate complex operation that should be atomic
updateUser(new User(fromUserId, "FromUser", -points));
updateUser(new User(toUserId, "ToUser", points));
// Simulate potential error
if (points < 0) {
throw new SQLException("Negative points transfer not allowed");
}
}
}
static class TransactionHandler implements InvocationHandler {
private final Object target;
private final Connection connection;
public TransactionHandler(Object target, Connection connection) {
this.target = target;
this.connection = connection;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Check if method is transactional
boolean isTransactional = method.isAnnotationPresent(Transactional.class);
if (!isTransactional) {
// Non-transactional method - just delegate
return method.invoke(target, args);
}
// Transactional method - wrap in transaction
boolean originalAutoCommit = connection.getAutoCommit();
try {
// Start transaction
connection.setAutoCommit(false);
System.out.println(">>> Starting transaction for: " + method.getName());
// Execute method
Object result = method.invoke(target, args);
// Commit transaction
connection.commit();
System.out.println(">>> Transaction committed for: " + method.getName());
return result;
} catch (Exception e) {
// Rollback transaction on error
connection.rollback();
System.out.println(">>> Transaction rolled back for: " + method.getName());
throw e;
} finally {
// Restore original auto-commit state
connection.setAutoCommit(originalAutoCommit);
}
}
}
public static void main(String[] args) throws Exception {
// Simulate database connection
Connection connection = DriverManager.getConnection("jdbc:hsqldb:mem:test", "sa", "");
UserRepository realRepository = new UserRepositoryImpl(connection);
UserRepository transactionalRepository = (UserRepository) Proxy.newProxyInstance(
UserRepository.class.getClassLoader(),
new Class<?>[] { UserRepository.class },
new TransactionHandler(realRepository, connection)
);
// Test successful transaction
System.out.println("=== Testing successful transaction ===");
try {
transactionalRepository.saveUser(new User(1, "John", 100));
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
System.out.println();
// Test failed transaction (rollback)
System.out.println("=== Testing failed transaction (rollback) ===");
try {
transactionalRepository.saveUser(new User(2, "error-user", 100));
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
System.out.println();
// Test complex transactional operation
System.out.println("=== Testing complex transactional operation ===");
try {
transactionalRepository.transferPoints(1, 2, 50);
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
System.out.println();
// Test non-transactional method
System.out.println("=== Testing non-transactional method ===");
User user = transactionalRepository.findUserById(1);
System.out.println("Found: " + user);
connection.close();
}
}

6. Dynamic Proxy Limitations and Considerations

Limitations and Workarounds

import java.lang.reflect.*;
public class ProxyLimitations {
interface Service {
void process();
}
static class ServiceImpl implements Service {
@Override
public void process() {
System.out.println("Processing...");
}
public void internalMethod() {
System.out.println("Internal method");
}
}
public static void main(String[] args) {
Service realService = new ServiceImpl();
Service proxy = (Service) Proxy.newProxyInstance(
Service.class.getClassLoader(),
new Class<?>[] { Service.class },
(p, method, args1) -> {
System.out.println("Intercepting: " + method.getName());
return method.invoke(realService, args1);
}
);
// This works - interface method
proxy.process();
// This won't work - proxy only implements the interface
// ((ServiceImpl) proxy).internalMethod(); // ClassCastException
System.out.println("\nProxy class: " + proxy.getClass());
System.out.println("Proxy superclass: " + proxy.getClass().getSuperclass());
System.out.println("Proxy interfaces: " + Arrays.toString(proxy.getClass().getInterfaces()));
System.out.println("Is proxy: " + Proxy.isProxyClass(proxy.getClass()));
// Get invocation handler
InvocationHandler handler = Proxy.getInvocationHandler(proxy);
System.out.println("Invocation handler: " + handler.getClass());
}
}

Key Benefits of Dynamic Proxies

  1. Aspect-Oriented Programming - Separate cross-cutting concerns
  2. Runtime Flexibility - Create proxies dynamically without source code
  3. Transparent Proxying - Clients don't know they're using proxies
  4. Interface-Based - Clean separation of interface and implementation
  5. Performance - Generally faster than reflection for repeated calls

Common Use Cases

  1. Logging and Monitoring - Method call tracing
  2. Caching - Transparent result caching
  3. Security - Access control and authorization
  4. Transactions - Automatic transaction management
  5. Lazy Loading - On-demand resource loading
  6. Remote Method Invocation - Network transparency
  7. Mocking - Test double creation

Dynamic proxies are a powerful feature that enables many advanced patterns in Java applications, particularly in frameworks like Spring, Hibernate, and other enterprise libraries.

Leave a Reply

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


Macro Nepal Helper