google-site-verification: google61fe8ba583a51912.html
Proxy Pattern for Controlled Access in Java

Introduction

The Proxy pattern provides a surrogate or placeholder for another object to control access to it. This structural pattern is used for lazy initialization, access control, logging, monitoring, and other cross-cutting concerns without modifying the original object's code.

Core Proxy Types

1. Virtual Proxy (Lazy Initialization)

/** * Interface for image operations */ public interface Image { void display(); String getFileName(); long getFileSize(); } /** * Real subject - expensive to create */ public class HighResolutionImage implements Image { private final String fileName; private final long fileSize; public HighResolutionImage(String fileName) { this.fileName = fileName; loadImageFromDisk(); // Expensive operation this.fileSize = calculateFileSize(); } private void loadImageFromDisk() { System.out.println("Loading high-resolution image: " + fileName); // Simulate expensive loading operation try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("Image loaded: " + fileName); } private long calculateFileSize() { // Simulate file size calculation return fileName.length() * 1024L; } @Override public void display() { System.out.println("Displaying high-resolution image: " + fileName); } @Override public String getFileName() { return fileName; } @Override public long getFileSize() { return fileSize; } } /** * Virtual Proxy - delays expensive object creation until needed */ public class ImageProxy implements Image { private final String fileName; private HighResolutionImage realImage; public ImageProxy(String fileName) { this.fileName = fileName; System.out.println("Image proxy created for: " + fileName); } private void initializeRealImage() { if (realImage == null) { realImage = new HighResolutionImage(fileName); } } @Override public void display() { initializeRealImage(); realImage.display(); } @Override public String getFileName() { return fileName; } @Override public long getFileSize() { initializeRealImage(); return realImage.getFileSize(); } // Additional proxy methods public boolean isImageLoaded() { return realImage != null; } public void preload() { initializeRealImage(); } }

2. Protection Proxy (Access Control)

/** * Database operation interface */ public interface Database { void query(String sql); void update(String sql); void delete(String sql); void createTable(String tableName); } /** * Real database implementation */ public class RealDatabase implements Database { private final String databaseName; public RealDatabase(String databaseName) { this.databaseName = databaseName; System.out.println("Connected to database: " + databaseName); } @Override public void query(String sql) { System.out.println("Executing query: " + sql); // Actual database query execution } @Override public void update(String sql) { System.out.println("Executing update: " + sql); // Actual database update } @Override public void delete(String sql) { System.out.println("Executing delete: " + sql); // Actual database delete } @Override public void createTable(String tableName) { System.out.println("Creating table: " + tableName); // Actual table creation } } /** * User roles for access control */ public enum UserRole { GUEST, // Can only query USER, // Can query and update ADMIN, // Can do everything DEVELOPER // Can create tables } /** * User entity */ public class User { private final String username; private final UserRole role; public User(String username, UserRole role) { this.username = username; this.role = role; } public String getUsername() { return username; } public UserRole getRole() { return role; } } /** * Protection Proxy - controls access based on user roles */ public class DatabaseProxy implements Database { private final Database realDatabase; private final User currentUser; public DatabaseProxy(String databaseName, User user) { this.realDatabase = new RealDatabase(databaseName); this.currentUser = user; System.out.println("Database proxy created for user: " + user.getUsername()); } private void checkPermission(UserRole requiredRole, String operation) { if (currentUser.getRole().ordinal() < requiredRole.ordinal()) { throw new SecurityException( "User '" + currentUser.getUsername() + "' with role '" + currentUser.getRole() + "' is not authorized to perform '" + operation + "'. Required role: " + requiredRole ); } } @Override public void query(String sql) { checkPermission(UserRole.GUEST, "QUERY"); System.out.println("Audit: User '" + currentUser.getUsername() + "' executed query: " + sql); realDatabase.query(sql); } @Override public void update(String sql) { checkPermission(UserRole.USER, "UPDATE"); System.out.println("Audit: User '" + currentUser.getUsername() + "' executed update: " + sql); realDatabase.update(sql); } @Override public void delete(String sql) { checkPermission(UserRole.ADMIN, "DELETE"); System.out.println("Audit: User '" + currentUser.getUsername() + "' executed delete: " + sql); realDatabase.delete(sql); } @Override public void createTable(String tableName) { checkPermission(UserRole.DEVELOPER, "CREATE_TABLE"); System.out.println("Audit: User '" + currentUser.getUsername() + "' created table: " + tableName); realDatabase.createTable(tableName); } // Proxy-specific methods public User getCurrentUser() { return currentUser; } public void changeUser(User newUser) { System.out.println("Switching user from " + currentUser.getUsername() + " to " + newUser.getUsername()); // In real implementation, we might create a new proxy or handle authentication } }

3. Remote Proxy (Network Communication)

/** * Bank account service interface */ public interface BankAccount { double getBalance(); void deposit(double amount); void withdraw(double amount) throws InsufficientFundsException; String getAccountNumber(); String getAccountHolder(); List<Transaction> getTransactionHistory(); } /** * Transaction record */ public class Transaction { private final String id; private final String type; private final double amount; private final Date timestamp; public Transaction(String type, double amount) { this.id = UUID.randomUUID().toString(); this.type = type; this.amount = amount; this.timestamp = new Date(); } // Getters public String getId() { return id; } public String getType() { return type; } public double getAmount() { return amount; } public Date getTimestamp() { return timestamp; } @Override public String toString() { return String.format("Transaction[%s: $%.2f at %s]", type, amount, timestamp); } } /** * Custom exception for banking operations */ public class InsufficientFundsException extends Exception { public InsufficientFundsException(String message) { super(message); } } /** * Real bank account implementation (on server side) */ public class RealBankAccount implements BankAccount { private final String accountNumber; private final String accountHolder; private double balance; private final List<Transaction> transactions; public RealBankAccount(String accountNumber, String accountHolder, double initialBalance) { this.accountNumber = accountNumber; this.accountHolder = accountHolder; this.balance = initialBalance; this.transactions = new ArrayList<>(); transactions.add(new Transaction("OPENING", initialBalance)); } @Override public double getBalance() { return balance; } @Override public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit amount must be positive"); } this.balance += amount; transactions.add(new Transaction("DEPOSIT", amount)); System.out.println("Deposited: $" + amount + " | New balance: $" + balance); } @Override public void withdraw(double amount) throws InsufficientFundsException { if (amount <= 0) { throw new IllegalArgumentException("Withdrawal amount must be positive"); } if (amount > balance) { throw new InsufficientFundsException( "Insufficient funds. Available: $" + balance + ", Requested: $" + amount ); } this.balance -= amount; transactions.add(new Transaction("WITHDRAWAL", amount)); System.out.println("Withdrawn: $" + amount + " | New balance: $" + balance); } @Override public String getAccountNumber() { return accountNumber; } @Override public String getAccountHolder() { return accountHolder; } @Override public List<Transaction> getTransactionHistory() { return new ArrayList<>(transactions); // Return copy for immutability } } /** * Remote Proxy - handles network communication transparently */ public class BankAccountProxy implements BankAccount { private final String accountNumber; private final String serverAddress; public BankAccountProxy(String accountNumber, String serverAddress) { this.accountNumber = accountNumber; this.serverAddress = serverAddress; System.out.println("Bank account proxy created for account: " + accountNumber); } // Simulate network communication private String sendRequest(String operation, String... parameters) { System.out.println("Sending request to " + serverAddress + ": " + operation); // Simulate network delay try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // Simulate server response switch (operation) { case "GET_BALANCE": return "1500.75"; case "GET_ACCOUNT_HOLDER": return "John Doe"; case "GET_TRANSACTIONS": return "DEPOSIT:500.00,2023-01-15;WITHDRAWAL:200.00,2023-01-20"; default: return "SUCCESS"; } } @Override public double getBalance() { String response = sendRequest("GET_BALANCE"); return Double.parseDouble(response); } @Override public void deposit(double amount) { sendRequest("DEPOSIT", String.valueOf(amount)); System.out.println("Deposit request sent: $" + amount); } @Override public void withdraw(double amount) throws InsufficientFundsException { try { sendRequest("WITHDRAW", String.valueOf(amount)); System.out.println("Withdrawal request sent: $" + amount); } catch (Exception e) { throw new InsufficientFundsException("Withdrawal failed: " + e.getMessage()); } } @Override public String getAccountNumber() { return accountNumber; } @Override public String getAccountHolder() { return sendRequest("GET_ACCOUNT_HOLDER"); } @Override public List<Transaction> getTransactionHistory() { String response = sendRequest("GET_TRANSACTIONS"); return parseTransactions(response); } private List<Transaction> parseTransactions(String response) { // Simplified parsing List<Transaction> transactions = new ArrayList<>(); String[] parts = response.split(";"); for (String part : parts) { String[] details = part.split(":"); if (details.length == 2) { String type = details[0]; double amount = Double.parseDouble(details[1]); transactions.add(new Transaction(type, amount)); } } return transactions; } }

4. Logging Proxy (Cross-Cutting Concerns)

/** * Data service interface */ public interface DataService { String fetchData(String query); void saveData(String data); void deleteData(String id); List<String> listAll(); } /** * Real data service implementation */ public class RealDataService implements DataService { private final Map<String, String> dataStore = new HashMap<>(); public RealDataService() { // Initialize with some sample data dataStore.put("1", "Sample Data 1"); dataStore.put("2", "Sample Data 2"); } @Override public String fetchData(String query) { System.out.println("Fetching data for query: " + query); // Simulate processing time try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return dataStore.getOrDefault(query, "No data found"); } @Override public void saveData(String data) { System.out.println("Saving data: " + data); String id = UUID.randomUUID().toString(); dataStore.put(id, data); } @Override public void deleteData(String id) { System.out.println("Deleting data with id: " + id); dataStore.remove(id); } @Override public List<String> listAll() { return new ArrayList<>(dataStore.values()); } } /** * Logging Proxy - adds logging without modifying original class */ public class LoggingProxy implements DataService { private final DataService realService; private final Logger logger; public LoggingProxy(DataService realService) { this.realService = realService; this.logger = Logger.getLogger(LoggingProxy.class.getName()); } @Override public String fetchData(String query) { long startTime = System.currentTimeMillis(); logger.info("Starting fetchData with query: " + query); try { String result = realService.fetchData(query); long duration = System.currentTimeMillis() - startTime; logger.info("fetchData completed in " + duration + "ms. Result: " + (result != null ? result.substring(0, Math.min(result.length(), 50)) + "..." : "null")); return result; } catch (Exception e) { logger.severe("fetchData failed for query: " + query + " - " + e.getMessage()); throw e; } } @Override public void saveData(String data) { logger.info("Starting saveData with data length: " + (data != null ? data.length() : 0)); try { realService.saveData(data); logger.info("saveData completed successfully"); } catch (Exception e) { logger.severe("saveData failed: " + e.getMessage()); throw e; } } @Override public void deleteData(String id) { logger.info("Starting deleteData with id: " + id); try { realService.deleteData(id); logger.info("deleteData completed successfully for id: " + id); } catch (Exception e) { logger.severe("deleteData failed for id: " + id + " - " + e.getMessage()); throw e; } } @Override public List<String> listAll() { logger.info("Starting listAll operation"); try { List<String> result = realService.listAll(); logger.info("listAll completed. Found " + result.size() + " items"); return result; } catch (Exception e) { logger.severe("listAll failed: " + e.getMessage()); throw e; } } }

5. Caching Proxy (Performance Optimization)

/** * Weather service interface */ public interface WeatherService { WeatherData getWeather(String city); WeatherData getWeather(double lat, double lon); List<WeatherData> getWeatherForecast(String city, int days); } /** * Weather data model */ public class WeatherData { private final String location; private final double temperature; private final String description; private final double humidity; private final double windSpeed; private final Date timestamp; public WeatherData(String location, double temperature, String description, double humidity, double windSpeed) { this.location = location; this.temperature = temperature; this.description = description; this.humidity = humidity; this.windSpeed = windSpeed; this.timestamp = new Date(); } // Getters public String getLocation() { return location; } public double getTemperature() { return temperature; } public String getDescription() { return description; } public double getHumidity() { return humidity; } public double getWindSpeed() { return windSpeed; } public Date getTimestamp() { return timestamp; } @Override public String toString() { return String.format("Weather[%s: %.1f°C, %s, Humidity: %.1f%%, Wind: %.1f m/s]", location, temperature, description, humidity, windSpeed); } } /** * Real weather service (makes actual API calls) */ public class RealWeatherService implements WeatherService { @Override public WeatherData getWeather(String city) { System.out.println("Making API call to get weather for: " + city); // Simulate API call delay try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // Simulate API response return new WeatherData(city, 20 + Math.random() * 15, "Sunny", 40 + Math.random() * 30, 5 + Math.random() * 10); } @Override public WeatherData getWeather(double lat, double lon) { System.out.println("Making API call to get weather for coordinates: " + lat + ", " + lon); try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return new WeatherData("Location(" + lat + "," + lon + ")", 15 + Math.random() * 20, "Cloudy", 50 + Math.random() * 25, 3 + Math.random() * 8); } @Override public List<WeatherData> getWeatherForecast(String city, int days) { System.out.println("Making API call to get " + days + "-day forecast for: " + city); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } List<WeatherData> forecast = new ArrayList<>(); for (int i = 0; i < days; i++) { forecast.add(new WeatherData(city, 18 + Math.random() * 12, i % 2 == 0 ? "Sunny" : "Rainy", 45 + Math.random() * 25, 4 + Math.random() * 6)); } return forecast; } } /** * Caching Proxy - improves performance by caching results */ public class CachingProxy implements WeatherService { private final WeatherService realService; private final Map<String, CacheEntry<WeatherData>> cache; private final long cacheTimeout; // milliseconds public CachingProxy(WeatherService realService, long cacheTimeout) { this.realService = realService; this.cache = new HashMap<>(); this.cacheTimeout = cacheTimeout; } private static class CacheEntry<T> { private final T data; private final long timestamp; public CacheEntry(T data) { this.data = data; this.timestamp = System.currentTimeMillis(); } public boolean isExpired(long timeout) { return System.currentTimeMillis() - timestamp > timeout; } public T getData() { return data; } } @Override public WeatherData getWeather(String city) { String cacheKey = "weather:" + city.toLowerCase(); CacheEntry<WeatherData> cached = cache.get(cacheKey); if (cached != null && !cached.isExpired(cacheTimeout)) { System.out.println("Returning cached weather for: " + city); return cached.getData(); } System.out.println("Cache miss for: " + city + ", fetching from service..."); WeatherData weather = realService.getWeather(city); cache.put(cacheKey, new CacheEntry<>(weather)); return weather; } @Override public WeatherData getWeather(double lat, double lon) { String cacheKey = String.format("weather:%.4f,%.4f", lat, lon); CacheEntry<WeatherData> cached = cache.get(cacheKey); if (cached != null && !cached.isExpired(cacheTimeout)) { System.out.println("Returning cached weather for coordinates"); return cached.getData(); } System.out.println("Cache miss for coordinates, fetching from service..."); WeatherData weather = realService.getWeather(lat, lon); cache.put(cacheKey, new CacheEntry<>(weather)); return weather; } @Override public List<WeatherData> getWeatherForecast(String city, int days) { String cacheKey = "forecast:" + city.toLowerCase() + ":" + days; @SuppressWarnings("unchecked") CacheEntry<List<WeatherData>> cached = (CacheEntry<List<WeatherData>>) cache.get(cacheKey); if (cached != null && !cached.isExpired(cacheTimeout)) { System.out.println("Returning cached forecast for: " + city); return cached.getData(); } System.out.println("Cache miss for forecast, fetching from service..."); List<WeatherData> forecast = realService.getWeatherForecast(city, days); cache.put(cacheKey, new CacheEntry<>(forecast)); return forecast; } // Cache management methods public void clearCache() { cache.clear(); System.out.println("Cache cleared"); } public int getCacheSize() { return cache.size(); } public void removeExpiredEntries() { int initialSize = cache.size(); cache.entrySet().removeIf(entry -> entry.getValue().isExpired(cacheTimeout)); int removed = initialSize - cache.size(); System.out.println("Removed " + removed + " expired cache entries"); } }

Dynamic Proxies (Java Reflection)

6. Dynamic Invocation Handler

/** * Generic dynamic proxy for method timing */ public class TimingDynamicProxy implements InvocationHandler { private final Object target; private final Map<String, MethodStats> methodStats; public TimingDynamicProxy(Object target) { this.target = target; this.methodStats = new ConcurrentHashMap<>(); } public static <T> T createProxy(T target, Class<T> interfaceClass) { return (T) Proxy.newProxyInstance( interfaceClass.getClassLoader(), new Class<?>[] { interfaceClass }, new TimingDynamicProxy(target) ); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long startTime = System.nanoTime(); String methodName = method.getName(); try { Object result = method.invoke(target, args); recordSuccess(methodName, System.nanoTime() - startTime); return result; } catch (InvocationTargetException e) { recordFailure(methodName, System.nanoTime() - startTime); throw e.getTargetException(); } } private void recordSuccess(String methodName, long duration) { MethodStats stats = methodStats.computeIfAbsent(methodName, k -> new MethodStats()); stats.recordSuccess(duration); } private void recordFailure(String methodName, long duration) { MethodStats stats = methodStats.computeIfAbsent(methodName, k -> new MethodStats()); stats.recordFailure(duration); } public void printStatistics() { System.out.println("\n=== Method Timing Statistics ==="); methodStats.forEach((methodName, stats) -> { System.out.printf("%s: %d calls, avg: %.2f ms, success: %d, failures: %d%n", methodName, stats.getTotalCalls(), stats.getAverageTime() / 1_000_000.0, stats.getSuccessCount(), stats.getFailureCount()); }); } private static class MethodStats { private long totalCalls = 0; private long successCount = 0; private long failureCount = 0; private long totalTime = 0; public void recordSuccess(long duration) { totalCalls++; successCount++; totalTime += duration; } public void recordFailure(long duration) { totalCalls++; failureCount++; totalTime += duration; } // Getters public long getTotalCalls() { return totalCalls; } public long getSuccessCount() { return successCount; } public long getFailureCount() { return failureCount; } public double getAverageTime() { return totalCalls > 0 ? (double) totalTime / totalCalls : 0; } } } /** * Service interface for dynamic proxy demonstration */ public interface UserService { User createUser(String username, String email); User findUserById(String id); void deleteUser(String id); List<User> listUsers(); } /** * User entity */ public class User { private final String id; private final String username; private final String email; private final Date createdAt; public User(String username, String email) { this.id = UUID.randomUUID().toString(); this.username = username; this.email = email; this.createdAt = new Date(); } // Getters public String getId() { return id; } public String getUsername() { return username; } public String getEmail() { return email; } public Date getCreatedAt() { return createdAt; } @Override public String toString() { return String.format("User[%s: %s, %s]", id, username, email); } }

Comprehensive Example: E-commerce System

7. Complete E-commerce Proxy System

/** * Payment service interface */ public interface PaymentService { PaymentResult processPayment(PaymentRequest request) throws PaymentException; PaymentStatus checkPaymentStatus(String paymentId); RefundResult processRefund(String paymentId, double amount) throws RefundException; } /** * Payment models */ public class PaymentRequest { private final String orderId; private final double amount; private final String currency; private final String cardNumber; private final String cardHolder; public PaymentRequest(String orderId, double amount, String currency, String cardNumber, String cardHolder) { this.orderId = orderId; this.amount = amount; this.currency = currency; this.cardNumber = cardNumber; this.cardHolder = cardHolder; } // Getters public String getOrderId() { return orderId; } public double getAmount() { return amount; } public String getCurrency() { return currency; } public String getCardNumber() { return cardNumber; } public String getCardHolder() { return cardHolder; } } public class PaymentResult { private final String paymentId; private final boolean success; private final String message; private final Date processedAt; public PaymentResult(String paymentId, boolean success, String message) { this.paymentId = paymentId; this.success = success; this.message = message; this.processedAt = new Date(); } // Getters public String getPaymentId() { return paymentId; } public boolean isSuccess() { return success; } public String getMessage() { return message; } public Date getProcessedAt() { return processedAt; } } public enum PaymentStatus { PENDING, SUCCESS, FAILED, REFUNDED } public class RefundResult { private final String refundId; private final boolean success; private final String message; public RefundResult(String refundId, boolean success, String message) { this.refundId = refundId; this.success = success; this.message = message; } // Getters public String getRefundId() { return refundId; } public boolean isSuccess() { return success; } public String getMessage() { return message; } } /** * Custom exceptions */ public class PaymentException extends Exception { public PaymentException(String message) { super(message); } public PaymentException(String message, Throwable cause) { super(message, cause); } } public class RefundException extends Exception { public RefundException(String message) { super(message); } public RefundException(String message, Throwable cause) { super(message, cause); } } /** * Real payment service (connects to actual payment gateway) */ public class RealPaymentService implements PaymentService { @Override public PaymentResult processPayment(PaymentRequest request) throws PaymentException { System.out.println("Processing payment for order: " + request.getOrderId()); // Simulate payment processing try { Thread.sleep(1000); // Simulate network delay // Simulate random failures for demonstration if (Math.random() < 0.1) { // 10% failure rate throw new PaymentException("Payment gateway timeout"); } if (request.getAmount() <= 0) { throw new PaymentException("Invalid amount: " + request.getAmount()); } String paymentId = "PAY-" + UUID.randomUUID().toString().substring(0, 8); return new PaymentResult(paymentId, true, "Payment processed successfully"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new PaymentException("Payment processing interrupted", e); } } @Override public PaymentStatus checkPaymentStatus(String paymentId) { System.out.println("Checking payment status for: " + paymentId); // Simulate status check try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // Return random status for demonstration PaymentStatus[] statuses = PaymentStatus.values(); return statuses[(int) (Math.random() * statuses.length)]; } @Override public RefundResult processRefund(String paymentId, double amount) throws RefundException { System.out.println("Processing refund for payment: " + paymentId); try { Thread.sleep(800); if (amount <= 0) { throw new RefundException("Invalid refund amount: " + amount); } String refundId = "REF-" + UUID.randomUUID().toString().substring(0, 8); return new RefundResult(refundId, true, "Refund processed successfully"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RefundException("Refund processing interrupted", e); } } } /** * Comprehensive Payment Service Proxy with multiple concerns */ public class ComprehensivePaymentProxy implements PaymentService { private final PaymentService realService; private final Map<String, PaymentResult> paymentCache; private final Map<String, PaymentStatus> statusCache; private final CircuitBreaker circuitBreaker; private final RateLimiter rateLimiter; public ComprehensivePaymentProxy(PaymentService realService) { this.realService = realService; this.paymentCache = new ConcurrentHashMap<>(); this.statusCache = new ConcurrentHashMap<>(); this.circuitBreaker = new CircuitBreaker(5, 30000); // 5 failures, 30s timeout this.rateLimiter = new RateLimiter(10, 60000); // 10 requests per minute } @Override public PaymentResult processPayment(PaymentRequest request) throws PaymentException { // 1. Rate limiting if (!rateLimiter.allowRequest()) { throw new PaymentException("Rate limit exceeded. Please try again later."); } // 2. Circuit breaker check if (!circuitBreaker.allowRequest()) { throw new PaymentException("Payment service is temporarily unavailable"); } // 3. Input validation validatePaymentRequest(request); String cacheKey = "payment:" + request.getOrderId(); // 4. Check cache PaymentResult cachedResult = paymentCache.get(cacheKey); if (cachedResult != null) { System.out.println("Returning cached payment result for order: " + request.getOrderId()); return cachedResult; } // 5. Process with circuit breaker try { PaymentResult result = realService.processPayment(request); // 6. Cache successful payments if (result.isSuccess()) { paymentCache.put(cacheKey, result); statusCache.put(result.getPaymentId(), PaymentStatus.SUCCESS); } circuitBreaker.recordSuccess(); return result; } catch (PaymentException e) { circuitBreaker.recordFailure(); throw e; } catch (Exception e) { circuitBreaker.recordFailure(); throw new PaymentException("Unexpected error during payment processing", e); } } @Override public PaymentStatus checkPaymentStatus(String paymentId) { // 1. Check cache first PaymentStatus cachedStatus = statusCache.get(paymentId); if (cachedStatus != null) { System.out.println("Returning cached status for payment: " + paymentId); return cachedStatus; } // 2. Rate limiting for status checks (more lenient) if (!rateLimiter.allowRequest()) { return PaymentStatus.PENDING; // Default to pending if rate limited } // 3. Circuit breaker for status checks if (!circuitBreaker.allowRequest()) { return PaymentStatus.PENDING; } try { PaymentStatus status = realService.checkPaymentStatus(paymentId); // 4. Cache the status statusCache.put(paymentId, status); circuitBreaker.recordSuccess(); return status; } catch (Exception e) { circuitBreaker.recordFailure(); return PaymentStatus.PENDING; // Default to pending on error } } @Override public RefundResult processRefund(String paymentId, double amount) throws RefundException { // Similar comprehensive handling for refunds if (!rateLimiter.allowRequest()) { throw new RefundException("Rate limit exceeded"); } if (!circuitBreaker.allowRequest()) { throw new RefundException("Service temporarily unavailable"); } try { RefundResult result = realService.processRefund(paymentId, amount); // Update cache if refund is successful if (result.isSuccess()) { statusCache.put(paymentId, PaymentStatus.REFUNDED); } circuitBreaker.recordSuccess(); return result; } catch (RefundException e) { circuitBreaker.recordFailure(); throw e; } catch (Exception e) { circuitBreaker.recordFailure(); throw new RefundException("Unexpected error during refund processing", e); } } private void validatePaymentRequest(PaymentRequest request) throws PaymentException { if (request == null) { throw new PaymentException("Payment request cannot be null"); } if (request.getOrderId() == null || request.getOrderId().trim().isEmpty()) { throw new PaymentException("Order ID is required"); } if (request.getAmount() <= 0) { throw new PaymentException("Amount must be positive"); } if (request.getCardNumber() == null || request.getCardNumber().trim().isEmpty()) { throw new PaymentException("Card number is required"); } if (!isValidCardNumber(request.getCardNumber())) { throw new PaymentException("Invalid card number"); } } private boolean isValidCardNumber(String cardNumber) { // Simple validation for demonstration String cleaned = cardNumber.replaceAll("\\s+", ""); return cleaned.length() >= 13 && cleaned.length() <= 19 && cleaned.matches("\\d+"); } // Proxy management methods public void clearCache() { paymentCache.clear(); statusCache.clear(); System.out.println("Payment cache cleared"); } public CircuitBreaker.State getCircuitBreakerState() { return circuitBreaker.getState(); } public int getCacheSize() { return paymentCache.size() + statusCache.size(); } } /** * Supporting classes for comprehensive proxy */ class CircuitBreaker { public enum State { CLOSED, OPEN, HALF_OPEN } private State state = State.CLOSED; private int failureCount = 0; private final int failureThreshold; private final long timeout; private long lastFailureTime; public CircuitBreaker(int failureThreshold, long timeout) { this.failureThreshold = failureThreshold; this.timeout = timeout; } public synchronized boolean allowRequest() { if (state == State.OPEN) { if (System.currentTimeMillis() - lastFailureTime > timeout) { state = State.HALF_OPEN; return true; } return false; } return true; } public synchronized void recordSuccess() { failureCount = 0; state = State.CLOSED; } public synchronized void recordFailure() { failureCount++; lastFailureTime = System.currentTimeMillis(); if (failureCount >= failureThreshold) { state = State.OPEN; } } public State getState() { return state; } } class RateLimiter { private final int maxRequests; private final long timeWindow; private final Queue<Long> requestTimes; public RateLimiter(int maxRequests, long timeWindow) { this.maxRequests = maxRequests; this.timeWindow = timeWindow; this.requestTimes = new LinkedList<>(); } public synchronized boolean allowRequest() { long currentTime = System.currentTimeMillis(); // Remove old requests outside the time window while (!requestTimes.isEmpty() && currentTime - requestTimes.peek() > timeWindow) { requestTimes.poll(); } // Check if within rate limit if (requestTimes.size() < maxRequests) { requestTimes.offer(currentTime); return true; } return false; } }

Testing the Proxy Patterns

Comprehensive Test Suite

import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import static org.junit.jupiter.api.Assertions.*; import java.lang.reflect.Proxy; import java.util.List; public class ProxyPatternTest { @Test public void testVirtualProxy() { System.out.println("\n=== Testing Virtual Proxy ==="); Image image = new ImageProxy("large_photo.jpg"); System.out.println("Image proxy created, real image not loaded yet"); assertFalse(((ImageProxy) image).isImageLoaded()); // Real image loaded only when needed image.display(); assertTrue(((ImageProxy) image).isImageLoaded()); } @Test public void testProtectionProxy() { System.out.println("\n=== Testing Protection Proxy ==="); User guest = new User("guest", UserRole.GUEST); User admin = new User("admin", UserRole.ADMIN); Database guestDb = new DatabaseProxy("test_db", guest); Database adminDb = new DatabaseProxy("test_db", admin); // Guest can query guestDb.query("SELECT * FROM users"); // Guest cannot update assertThrows(SecurityException.class, () -> { guestDb.update("UPDATE users SET name='test'"); }); // Admin can update adminDb.update("UPDATE users SET name='test'"); } @Test public void testCachingProxy() { System.out.println("\n=== Testing Caching Proxy ==="); WeatherService realService = new RealWeatherService(); WeatherService cachingService = new CachingProxy(realService, 5000); // 5 second cache // First call - should call real service WeatherData weather1 = cachingService.getWeather("London"); // Second call - should return from cache WeatherData weather2 = cachingService.getWeather("London"); assertEquals(weather1.getLocation(), weather2.getLocation()); // Verify cache is working assertEquals(1, ((CachingProxy) cachingService).getCacheSize()); } @Test public void testLoggingProxy() { System.out.println("\n=== Testing Logging Proxy ==="); DataService realService = new RealDataService(); DataService loggingService = new LoggingProxy(realService); String result = loggingService.fetchData("1"); assertNotNull(result); loggingService.saveData("New data"); loggingService.listAll(); } @Test public void testDynamicProxy() { System.out.println("\n=== Testing Dynamic Proxy ==="); UserService realService = new UserService() { @Override public User createUser(String username, String email) { return new User(username, email); } @Override public User findUserById(String id) { return new User("test", "[email protected]"); } @Override public void deleteUser(String id) { System.out.println("Deleting user: " + id); } @Override public List<User> listUsers() { return List.of(new User("user1", "[email protected]")); } }; UserService proxiedService = TimingDynamicProxy.createProxy(realService, UserService.class); User user = proxiedService.createUser("john", "[email protected]"); assertNotNull(user); proxiedService.findUserById("123"); proxiedService.listUsers(); // Print timing statistics ((TimingDynamicProxy) Proxy.getInvocationHandler(proxiedService)).printStatistics(); } @Test public void testComprehensivePaymentProxy() throws PaymentException { System.out.println("\n=== Testing Comprehensive Payment Proxy ==="); PaymentService realService = new RealPaymentService(); ComprehensivePaymentProxy paymentProxy = new ComprehensivePaymentProxy(realService); PaymentRequest request = new PaymentRequest( "ORDER-123", 100.0, "USD", "4111111111111111", "John Doe" ); PaymentResult result = paymentProxy.processPayment(request); assertTrue(result.isSuccess()); // Check cache assertEquals(1, paymentProxy.getCacheSize()); // Check status (should be cached) PaymentStatus status = paymentProxy.checkPaymentStatus(result.getPaymentId()); assertNotNull(status); } }

Best Practices and Considerations

When to Use Proxy Pattern

public class ProxyBestPractices { /** * Appropriate use cases for Proxy Pattern: * * 1. Lazy Initialization (Virtual Proxy) * - When object creation is expensive * - When object might not be used immediately * * 2. Access Control (Protection Proxy) * - When you need to control access to sensitive operations * - When you need different access levels for different users * * 3. Remote Communication (Remote Proxy) * - When dealing with remote services or distributed systems * - When you want to hide network complexity * * 4. Caching (Cache Proxy) * - When you want to cache expensive operations * - When you need to reduce network calls or computation * * 5. Logging and Monitoring (Logging Proxy) * - When you need to add cross-cutting concerns * - When you want to monitor method calls and performance * * 6. Circuit Breaker and Resilience * - When you need to add fault tolerance * - When dealing with unreliable external services */ // Good example: Service with multiple concerns public interface ExternalService { Data fetchData(String id); void updateData(String id, Data data); } public class ComprehensiveServiceProxy implements ExternalService { private final ExternalService realService; private final Cache cache; private final CircuitBreaker circuitBreaker; private final Logger logger; public ComprehensiveServiceProxy(ExternalService realService) { this.realService = realService; this.cache = new Cache(); this.circuitBreaker = new CircuitBreaker(); this.logger = Logger.getLogger(getClass().getName()); } @Override public Data fetchData(String id) { // 1. Check cache Data cached = cache.get(id); if (cached != null) { logger.info("Returning cached data for: " + id); return cached; } // 2. Circuit breaker check if (!circuitBreaker.allowRequest()) { throw new ServiceUnavailableException("Service temporarily unavailable"); } // 3. Logging logger.info("Fetching data for: " + id); long startTime = System.currentTimeMillis(); try { Data result = realService.fetchData(id); // 4. Cache successful results cache.put(id, result); // 5. Record success circuitBreaker.recordSuccess(); logger.info("Data fetched successfully in " + (System.currentTimeMillis() - startTime) + "ms"); return result; } catch (Exception e) { // 6. Record failure circuitBreaker.recordFailure(); logger.error("Failed to fetch data for: " + id, e); throw e; } } @Override public void updateData(String id, Data data) { // Similar comprehensive handling for updates // Invalidate cache, handle failures, etc. } } } /** * Considerations and Anti-patterns: * * 1. Don't use proxy when simple composition would suffice * 2. Be careful with deep proxy chains (performance impact) * 3. Ensure proxy doesn't change the expected behavior * 4. Consider using frameworks (Spring AOP) for cross-cutting concerns * 5. Be mindful of serialization issues with proxies */ public class ProxyConsiderations { // Anti-pattern: Overusing proxies for simple tasks public class OverEngineeredProxy implements SimpleService { private final SimpleService realService; private final List<MethodInterceptor> interceptors; // Too complex for simple service } // Better: Use proxy only when you have clear benefits public class SimpleServiceProxy implements SimpleService { private final SimpleService realService; public SimpleServiceProxy(SimpleService realService) { this.realService = realService; } @Override public String getData() { // Add only necessary functionality return realService.getData(); } } }

Conclusion

The Proxy pattern is a powerful structural pattern that provides controlled access to objects. Key benefits include:

  • Lazy initialization for expensive objects
  • Access control and security enforcement
  • Performance optimization through caching
  • Cross-cutting concerns without modifying original code
  • Remote communication abstraction
  • Resilience patterns implementation

The pattern is particularly valuable in distributed systems, security-sensitive applications, and performance-critical scenarios where you need to add functionality without changing the core business logic.

Leave a Reply

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


Macro Nepal Helper