Что такое проксирование в Hibernate и зачем оно нужно
Что проверяют
Понимаете ли вы, что ленивая загрузка это не галочка в настройках, а подмена объекта: вместо сущности в поле лежит сгенерированный наследник, который сходит в базу при первом обращении. Отсюда растут исключение о закрытой сессии, проблема N+1 и сломанное сравнение объектов, поэтому вопрос любят как вход в разговор о производительности.
Как отвечать
Hibernate генерирует байт-код подкласса сущности и подставляет его экземпляр там, где данные ещё не нужны. Такой заместитель хранит идентификатор и ссылку на сессию, а при первом вызове любого метода, кроме геттера идентификатора, выполняет запрос и заполняет себя.
Где это происходит: получение ссылки по идентификатору отдаёт заместителя без похода в базу, связи @ManyToOne и @OneToOne с FetchType.LAZY подставляются им же. Коллекции устроены иначе: там не заместитель сущности, а собственная реализация списка или множества, которая грузит содержимое при первом обращении.
Требования к классу сущности: не финальный, методы не финальные, есть конструктор без аргументов хотя бы с доступом protected. Иначе заместителя не построить.
Инструменты, которые стоит назвать: проверка инициализации, принудительная инициализация и разворачивание заместителя из Hibernate, а для запроса заранее join fetch и графы сущностей.
Где спотыкаются
Ловят исключение о работе с незагруженным полем вне сессии и лечат его расширением транзакции на весь запрос. Правильнее собрать нужные данные внутри транзакции: join fetch, граф сущностей или проекция сразу в DTO.
Сравнивают сущности по getClass. У заместителя класс другой, и для одной и той же строки таблицы сравнение даёт ложь. Нужны instanceof, бизнес-ключ и разворачивание заместителя перед сравнением.
Отдают сущности наружу как есть. Сериализация незагруженного поля в JSON либо падает, либо дёргает базу уже после закрытия транзакции, а в списке превращается в N+1 запрос. Наружу уходит отдельный объект передачи данных.