- PVSM.RU - https://www.pvsm.ru -

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

Disclaimer

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

Предыстория

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

  • Persistence: Hibernate.
  • Services: Spring.
  • Endpoints: Spring MVC: JSP, RESTful.

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

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

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

Listener'ы и Interceptor'ы

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

Mapping

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

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 [5].

Автор: moonster

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/java/64595

Ссылки в тексте:

[1] способ побороть недочеты проектирования: http://ru.wikipedia.org/wiki/%D0%9F%D0%B0%D0%BB%D0%BB%D0%B8%D0%B0%D1%82%D0%B8%D0%B2

[2] Listener'ы: https://docs.jboss.org/hibernate/entitymanager/3.6/reference/en/html/listeners.html

[3] Interceptor'ы: http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch14.html

[4] ленивая загрузка: http://docs.jboss.org/hibernate/orm/4.3/manual/en-US/html/ch20.html#performance-fetching

[5] Google Drive: https://docs.google.com/file/d/0B5b3LGTun9xbTUZSbkVVZWw3THM/edit

[6] Источник: http://habrahabr.ru/post/223971/