Proxy (Заместитель)
Proxy контролирует доступ к другому объекту, выступая его заместителем.
Почему мы вообще хотим контролировать доступ? Одной из причин может быть то, что создание реального объекта обходится “дорого”, поэтому с помощью Proxy мы откладываем его создание до того, как объект действительно понадобится.
Пример
Представим, что у нас есть JSON файл, в котором хранятся мировые географические достопримечательности. Содержимое файла выглядит так:
[
{"name": "Grand Canyon", "type": "NATURAL_LANDMARK", "latitude": 36.1069, "longitude": -112.1129},
{"name": "Great Wall of China", "type": "HISTORICAL_SITE", "latitude": 40.4319, "longitude": 116.5704},
{"name": "Mount Everest", "type": "MOUNTAIN_PEAK", "latitude": 27.9881, "longitude": 86.9250},
...
]textВ файле может быть очень много объектов. Эти достопримечательности нам иногда нужно выводить пользователю, однако функционал используется не часто.
Для доступа к этой информации в приложении есть интерфейс и несколько классов:
package geography;
import java.util.List;
public interface GeographicalData {
List<GeographicalObject> allGeographicalObjects();
List<GeographicalObject> filterByType(GeographicalObjectType type);
}javapackage geography;
public record Coordinates(double latitude, double longitude) {}javapackage geography;
public enum GeographicalObjectType {
NATURAL_LANDMARK,
HISTORICAL_SITE,
MOUNTAIN_PEAK
}javapackage geography;
public record GeographicalObject(
String name,
GeographicalObjectType type,
Coordinates coordinates
) {}javapackage geography;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class RealGeographicalData implements GeographicalData {
private final List<GeographicalObject> objects;
public RealGeographicalData(Path jsonFile) {
// Загрузка данных происходит в конструкторе –
// все объекты читаются из файла сразу при создании
this.objects = List.copyOf(loadFromFile(jsonFile));
}
@Override
public List<GeographicalObject> allGeographicalObjects() {
return objects;
}
@Override
public List<GeographicalObject> filterByType(GeographicalObjectType type) {
return objects.stream()
.filter(obj -> obj.type() == type)
.toList();
}
private static List<GeographicalObject> loadFromFile(Path jsonFile) {
// Представим, что файл содержит тысячи объектов
// и его загрузка занимает несколько секунд
try {
String content = Files.readString(jsonFile);
return parseGeographicalObjects(content);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static List<GeographicalObject> parseGeographicalObjects(String json) {
// Парсинг JSON опущен для краткости
return List.of();
}
}javaОчевидно, что перед тем как работать с RealGeographicalData, необходимо прочитать и распарсить JSON файл с географическими объектами. Конструктор делает это сразу при создании.
Но поскольку файл довольно тяжёлый, хотелось бы отложить чтение файла на самый последний момент – когда информация из него действительно понадобится.
Здесь нам на помощь приходит паттерн Proxy. Создадим класс-заместитель:
package geography;
import java.nio.file.Path;
import java.util.List;
public final class GeographicalDataProxy implements GeographicalData {
private final Path jsonFile;
private RealGeographicalData realData;
public GeographicalDataProxy(Path jsonFile) {
// Конструктор не загружает данные – только сохраняет путь к файлу
this.jsonFile = jsonFile;
}
@Override
public List<GeographicalObject> allGeographicalObjects() {
return getRealData().allGeographicalObjects();
}
@Override
public List<GeographicalObject> filterByType(GeographicalObjectType type) {
return getRealData().filterByType(type);
}
// В многопоточной среде потребуется синхронизация
private RealGeographicalData getRealData() {
if (realData == null) {
// Данные загружаются только при первом обращении
realData = new RealGeographicalData(jsonFile);
}
return realData;
}
}javaКласс GeographicalDataProxy реализует тот же интерфейс, что и RealGeographicalData. Обратим внимание на метод getRealData(): прокси проверяет, загрузили ли мы данные из файла. Если ещё нет – создаёт RealGeographicalData, который прочитает и распарсит файл.
Таким образом, мы можем быть уверены, что данные из файла будут загружены только при первом обращении. Всё остальное время GeographicalDataProxy играет роль «заглушки» и экономит ресурсы:
// Создание прокси – мгновенное, данные ещё не загружены
GeographicalData data = new GeographicalDataProxy(Path.of("geographical-objects.json"));
// ...
// Данные загружаются только сейчас, при первом вызове
List<GeographicalObject> cities = data.filterByType(GeographicalObjectType.HISTORICAL_SITE);javaКлиентский код работает с интерфейсом GeographicalData и не знает, обращается ли он к реальному объекту или к прокси. Именно в этом ключевое свойство паттерна – прокси подменяет оригинальный объект незаметно для клиента.
Реальные примеры
java.lang.reflect.Proxy
java.lang.reflect.Proxy позволяет создать прокси-объект для любого интерфейса в рантайме. Вместо написания отдельного класса-прокси для каждого интерфейса мы определяем единый InvocationHandler, который перехватывает все вызовы методов:
package real;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.logging.Logger;
public final class LoggingHandler implements InvocationHandler {
private static final Logger logger = Logger.getLogger(LoggingHandler.class.getName());
private final Object target;
private LoggingHandler(Object target) {
this.target = target;
}
// Class<Map> не хранит параметры типа (type erasure),
// поэтому каст к T – unchecked
@SuppressWarnings("unchecked")
public static <T> T create(T target, Class<T> iface) {
return (T) Proxy.newProxyInstance(
iface.getClassLoader(),
new Class<?>[]{iface},
new LoggingHandler(target)
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
logger.info("Вызов: " + method.getName());
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
}javaMap<String, String> map = LoggingHandler.create(Map.of("key", "value"), Map.class);
map.get("key"); // логгирует: "Вызов: get"javajava.lang.reflect.Proxy – основа для множества фреймворков. Например, Spring использует динамические прокси для реализации AOP, транзакций и lazy-загрузки бинов.
Hibernate lazy loading
Hibernate ↗ использует прокси для ленивой загрузки связанных сущностей. Когда мы загружаем объект из базы данных, Hibernate может не загружать его связи сразу – вместо этого он подставляет прокси-объект, который загрузит данные при первом обращении.
Для примера возьмём сущности «отдел» и «сотрудник»:
import jakarta.persistence.*;
import java.util.List;
@Entity
public class Department {
@Id
@GeneratedValue
private Long id;
private String name;
// По умолчанию Hibernate не загружает сотрудников сразу –
// вместо этого employees будет прокси-коллекцией
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
private List<Employee> employees;
public List<Employee> getEmployees() {
return employees;
}
}
@Entity
public class Employee {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
private Department department;
}java// Hibernate загружает только Department – employees пока не загружены
Department dept = entityManager.find(Department.class, 1L);
// getEmployees() возвращает прокси-коллекцию, данные ещё не загружены
List<Employee> employees = dept.getEmployees();
// SQL-запрос выполняется сейчас – при первом обращении к содержимому коллекции
int count = employees.size();javaПри загрузке Department Hibernate подставляет вместо обычного List<Employee> специальную коллекцию-обёртку. При первом обращении к ней (например, вызове getEmployees().size()) обёртка выполняет SQL-запрос и подгружает реальные данные. Клиентский код работает с List<Employee> как обычно и не знает о подмене.
Таким образом, Hibernate откладывает обращение к относительно медленному ресурсу (базе данных) до момента, когда данные действительно понадобятся – тот же принцип, что и в нашем примере с GeographicalDataProxy.
httputil.ReverseProxy
httputil.ReverseProxy из стандартной библиотеки Go – это прокси, который перенаправляет HTTP-запросы на другой сервер. Он реализует интерфейс http.Handler, поэтому клиентский код работает с ним так же, как с любым другим обработчиком:
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
// URL сервера, к которому проксируются запросы
target, err := url.Parse("http://localhost:9090")
if err != nil {
log.Fatal(err)
}
// ReverseProxy – прокси: перехватывает запросы
// и перенаправляет их на целевой сервер
proxy := httputil.NewSingleHostReverseProxy(target)
// Клиентский код работает с http.Handler – ему не важно,
// обрабатывается ли запрос локально или проксируется
http.Handle("/api/", proxy)
log.Fatal(http.ListenAndServe(":8080", nil))
}goReverseProxy контролирует доступ к удалённому серверу: пробрасывает заголовки, отсеивая hop-by-hop, и проксирует тело запроса. Клиентский код видит лишь интерфейс http.Handler и не знает, что за ним стоит другой сервер.