Динамическое добавление свойств в языке Java

    Disclaimer

    Инструмент родился как способ побороть недочеты проектирования малой кровью. Я с трудом могу представить ситуацию, где использование инструмента могло бы быть продиктовано иными причинами.

    Предыстория

    Случилось мне однажды подключиться к разработке немолодого web-приложения. Код был местами изрядно запутан, хранил следы деятельности нескольких разработчиков разной квалификации, актуальных работоспособных тестов не было. Одно слово — legacy.
    Приложение было реализовано по классической трехслойной схеме:
    • Persistence: Hibernate.
    • Services: Spring.
    • Endpoints: Spring MVC: JSP, RESTful.

    И на всех уровнях использовались сущности уровня хранения (далее — сущности). Из-за этого изменение имен свойств сущностей влекло за собой необходимость править код на клиенте или в JSP, а добавление чего-нибудь в JSON который отдавали контроллеры или в JSP, если это что-то не лежало в сущности, было крайне неудобным и рискованным из за отсутствия тестов.

    Как же протащить на уровень представления значения, которых нет в сущностях?

    Я рассмотрел несколько способов.

    Listener'ы и Interceptor'ы

    Listener'ы и Interceptor'ы позволяют добавить в сущности дополнительные данные. В некоторых случаях их применение оправдано, но предпочитаю не засорять сущности уровня хранения структурами и данными, не имеющими к уровню хранения никакого отношения.

    Mapping

    Можно пронаследоваться от каждого класса, который требует расширения, и начинить его нужными данными уже на сервисном уровне. Это концептуально верный путь: слой хранения сохраняет чистоту, слой контроллеров не требует модификации. Однако, возникают проблемы с производительностью, ленивая загрузка перестает быть ленивой, т.к. маппер не знает, какие из полей нужны контроллеру, и вынужден перекладывать все. Управление маппером со стороны контроллера теоретически возможно, но это невозможно без модификации кода контроллера.

    Dynamic proxy

    Немножко магии:
    package ru.bdm.reflection;
    
    //some imports omitted
    
    import static junit.framework.Assert.assertEquals;
    import static org.apache.commons.beanutils.PropertyUtils.getProperty;
    
    public class PropertyJoinerTest {
    
        public static class AnyType {
            public Object getAnyProperty() {
                return "anyPropertyValue";
            }
        }
    
        @Test
        public void testWithPropertyExtractor() throws Exception {
            PropertyJoiner propertyJoiner = new PropertyJoiner(new PropertyExtractor() {
                @Override
                public Object get(Object o, String property) {
                    return property + "Value";
                }
            }, "first", "second");
    
            AnyType src = new AnyType();
    
            AnyType dst = propertyJoiner.joinProperties(src);
    
            assertEquals("firstValue", getProperty(dst, "first"));
            assertEquals("secondValue", getProperty(dst, "second"));
            assertEquals("anyPropertyValue", getProperty(dst, "anyProperty"));
        }
    }
    

    Что под капотом?

    Динамически создаются классы интерфейсов для добавочных свойств:
    public interface FirstHolder{
       Object getFirst();
    }
    public interface SecondHolder{
       Object getSecond();
    }
    

    Динамически создается класс proxy, который наследует AnyType и реализует FirstHolder и SecondHolder.
    Методы, определенные в AnyType, proxy перенаправляет к src, методы, определенные в FirstHolder и SecondHolder, перенаправляются в PropertyExtractor, который содержит логику вычисления добавочных свойств.

    Таким образом мы получили возможность расширения представления, не меняя при этом код контроллеров и не засоряя сущности уровня хранения посторонними структурами и данными, не получая падения производительности из-за проблем с ленивой загрузкой.

    Плата за это оказалась не очень велика: доступ к свойствам через прокси примерно в 150 раз медленнее, чем непосредственный. Это стоит учитывать при использовании инструмента.
    Нагрузка нашего приложения была всего несколько запросов в секунду, за каждый запрос читалось максимум 50 сущностей (размер страницы), так что долей потерь в proxy можно было пренебречь.

    Скачать код можно с Google Drive.
    Поделиться публикацией

    Комментарии 4

      0
      Зачем же так сложно.
      Связь DTO и Entity лучше сделать обычными сеттерами и геттерами. Вы так усложняете чтение кода и жертвуете производительностью.
        0
        Вы совершенно правы. Вот только в этом проекте никаких DTO не было. Мне досталось около 30000 строк кода, в которых не было ни строчки про DTO.
          0
          Это хорошо, когда пишем проект с нуля.
          А в случае работающего большого проекта, где что-то нужно регулярно подпиливать, это плохо работает — времени-то на это не выделяют, а подобное изменение в одном месте может вызвать лавинообразное изменение по всему коду.
          Не зря было сказано про legacy.
            0
            По вашем скопировать все entity, переименовать их с суффиксом dto, сделать их serializable и удалить не нужные поля это долго?
            По моему лучше уж сесть 1 раз и заняться рутиной, чем потом всю жизнь мучатся.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое