BackendBit

К списку паттернов

Proxy контролирует доступ к другому объекту, выступая его заместителем.

Почему мы вообще хотим контролировать доступ? Одной из причин может быть то, что создание реального объекта обходится “дорого”, поэтому с помощью Proxy мы откладываем его создание до того, как объект действительно понадобится.

UML диаграмма классов паттерна ЗаместительПоказывает интерфейс GeographicalData с методами allGeographicalObjects() и filterByType(), реализованный классами RealGeographicalData и GeographicalDataProxy. Прокси хранит ссылку на реальный объект и делегирует ему вызовы.<<interface>>GeographicalData+allGeographicalObjects(): List+filterByType(Type): ListGeographicalDataProxy+realData: RealGeographicalDataRealGeographicalDataРеальный объектПрокси – делегирует вызовыреальному объекту

Пример

Представим, что у нас есть 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);
}
java
package geography;

public record Coordinates(double latitude, double longitude) {}
java
package geography;

public enum GeographicalObjectType {
    NATURAL_LANDMARK,
    HISTORICAL_SITE,
    MOUNTAIN_PEAK
}
java
package geography;

public record GeographicalObject(
    String name,
    GeographicalObjectType type,
    Coordinates coordinates
) {}
java
package 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();
        }
    }
}
java
Map<String, String> map = LoggingHandler.create(Map.of("key", "value"), Map.class);
map.get("key"); // логгирует: "Вызов: get"
java

java.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))
}
go

ReverseProxy контролирует доступ к удалённому серверу: пробрасывает заголовки, отсеивая hop-by-hop, и проксирует тело запроса. Клиентский код видит лишь интерфейс http.Handler и не знает, что за ним стоит другой сервер.