Статьи

Домен-управляемый дизайн с Spring и AspectJ

В предыдущем посте, размещенном на JavaCodeGeeks, наш партнер по введение в доменно- управляемый дизайн с использованием шаблона проектирования State . В конце этого руководства он признал, что не учел процесс внедрения зависимостей (DAO, бизнес-сервисов и т. Д.) В доменные объекты. Однако он обещал объяснить подробности в следующем посте. Что ж, теперь пришло время посмотреть, как это достигается. Томаш продолжает очень провокационный урок .

Посмотрим, что он скажет:

(ПРИМЕЧАНИЕ: оригинальный пост был слегка отредактирован для улучшения читабельности)

Прежде чем я начну обсуждать нашу основную тему, я хотел бы, чтобы вы немного подумали о лучшем дизайне приложений Java EE, который вы можете себе представить. Неважно, используете ли вы Spring или EJB3, поскольку они очень похожи, возможно, вы предложите подход, подобный следующему. Начиная с серверной части у вас есть:

  • доменные объекты , которые являются простыми объектами POJO, сопоставленными непосредственно с отношениями базы данных. POJO хороши тем, что свойства в стиле JavaBean хорошо понятны для многих фреймворков.
  • Уровень доступа к данным — обычно сервисы без сохранения состояния, которые заключают в себе код доступа к базе данных (JDBC, Hibernate, JPA, iBatis или все, что вы хотите), скрывая его сложность и обеспечивая некоторый уровень (утечки) абстракции. DAO хороши тем, что скрывают неприятную и неуклюжую логику JDBC (вот почему некоторые подвергают сомнению необходимость использования DAO при использовании JPA), выступая в качестве более или менее переводчика между базой данных и объектами.
  • Уровень бизнес-сервисов — еще один набор сервисов без сохранения состояния, которые работают с объектами домена. Типичный дизайн представляет граф объектов, которые принимают или возвращают доменные объекты и выполняют с ними некоторую логику, опять же, обычно обращаясь к базе данных через уровень доступа к данным. Уровень обслуживания хорош, потому что он фокусируется на бизнес-логике, делегируя технические детали уровню DAO.
  • пользовательский интерфейс — в настоящее время, как правило, через веб-браузер. Пользовательский интерфейс великолепен, потому что … просто факт.

Красиво, не правда ли? Теперь откройте глаза, пришло время для холодного душа.

И сервисы, и DAO не имеют статуса, потому что Spring и EJB3 предпочитают такие классы — поэтому мы научились жить с этим. С другой стороны, POJO «без логики» — они содержат только данные, поддерживают свое состояние, не оперируя ими и не вводя логику. Если мы думаем о введении объекта домена «резервирование» в наше приложение, мы сразу же думаем о POJO резервирования, сопоставленных с таблицей базы данных RESERVATIONS, ReservationDao, ReservationService, ReservationController и т. Д.

Все еще не видите проблему? Как бы вы описали «объект»? Это виртуальное существо, имеющее внутреннее состояние (инкапсуляция) и некоторые открытые операции, которые имеют явный доступ к этому состоянию. Наиболее фундаментальная концепция объектно-ориентированного программирования состоит в том, чтобы объединить данные и процедуры, работающие с этими данными, и плотно их закрыть. Теперь взгляните на ваш лучший дизайн, вам действительно нужны объекты? Это темный секрет Spring, EJB3, Hibernate и других хорошо зарекомендовавших себя фреймворков. Секрет, который все мы подсознательно пытаемся забыть: мы больше не программисты ООП!

POJO не являются объектами, они просто структуры данных, коллекции данных. На самом деле, методы получения и установки не являются истинными методами, когда вы в последний раз писали их вручную? Фактически, необходимость их автоматической генерации (а также рефакторинга, добавления и удаления при изменении атрибутов) иногда бывает очень неприятной. Не проще ли использовать структуры с открытыми полями по умолчанию?

С другой стороны, посмотрите на все эти замечательные службы без гражданства. У них нет никакого государства. Хотя они работают с объектами домена, они не являются их частью или даже не объединяют их (низкая когезия). Все данные передаются явно через параметры метода. Они также не являются объектами — это просто набор процедур, произвольно собранных в общем пространстве имен, соответствующих имени класса. В контрактах методы в ООП также являются негласными процедурами, но имеют неявный доступ к этой ссылке, которая указывает на экземпляр объекта. Всякий раз, когда мы вызываем ReservationService или ReservationDao, явно предоставляя ссылку на POJO для резервирования в качестве одного из аргументов, мы фактически заново изобретаем ООП и кодируем его вручную.

Посмотрим правде в глаза, мы не программисты ООП, поскольку все, что нам нужно, это структуры и процедуры, изобретенные пятьдесят лет назад. Сколько Java-программистов используют наследование и полиморфизм в повседневной жизни? Когда в последний раз вы писали объект с закрытым состоянием без методов получения / установки, и только несколько методов имели к нему доступ? Когда вы в последний раз создавали объект с конструктором не по умолчанию?

К счастью, то, что взяла Весна, возвращает с еще большей силой. Эта сила называется AspectJ .

В своем последнем посте я создал сущность Reservation, имеющую три бизнес-метода: accept (), charge () и cancel (). Выглядит очень хорошо, если бизнес-методы, относящиеся к объекту домена, размещаются непосредственно в этом объекте. Вместо того, чтобы вызывать bookingService.accept (booking), мы просто запускаем booking.accept (), который намного более интуитивен и менее шумен. Еще лучше, как насчет письма:

1
2
3
Reservation res = new Reservation()
//...
res.persist()

вместо того, чтобы вызывать DAO или использовать EntityManager напрямую? Я не знаю много о доменно-ориентированном дизайне, но я нашел этот фундаментальный рефакторинг первым, через который вы должны пройти, чтобы войти в мир DDD (и вернуться к ООП).

Для метода резервирования accept () в конечном итоге потребуется делегировать некоторую логику внешним службам, таким как учет или отправка электронной почты. Естественно, эта логика не является частью объекта домена Reservation и должна быть реализована в другом месте (высокая сплоченность). Дело в том, как внедрить другие сервисы в доменные объекты. Когда все сервисы управляются Spring, все просто. Но когда Hibernate сам создает доменные объекты или объект создается с помощью оператора new, Spring не знает об этом экземпляре и не может обрабатывать внедрение зависимостей. Так как же Reservation POJO получит Spring bean-компоненты или EntityManager, инкапсулирующие необходимую логику?

Сначала добавьте аннотацию @Configurable к объекту вашего домена:

1
2
3
4
5
@Configurable
@Entity
public class Reservation implements Serializable {
  //...
}

Это говорит Spring, что резервирование POJO должно осуществляться Spring. Но, как упоминалось выше, Spring не знает о создаваемых экземплярах Reservation, поэтому у него нет возможности автоматически связывать и вставлять зависимости. Это где AspectJ входит. Все, что вам нужно сделать, это добавить:

1
<context:load-time-weaver/>

к вашему XML-дескриптору Spring. Этот чрезвычайно короткий фрагмент XML сообщает Spring, что он должен использовать AspectJ weaving-weaving (LTW). Теперь, когда вы запускаете ваше приложение:

java.lang.IllegalStateException: ClassLoader [org.apache.catalina.loader.WebappClassLoader] НЕ предоставляет метод addTransformer (ClassFileTransformer). Укажите пользовательский LoadTimeWeaver или запустите виртуальную машину Java с агентом Spring: -javaagent: spring-agent.jar
в org.springframework.context.weaving.DefaultContextLoadTimeWeaver.
setBeanClassLoader (DefaultContextLoadTimeWeaver.java:82)
в org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.
initializeBean (AbstractAutowireCapableBeanFactory.java:1322)
в org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.
doCreateBean (AbstractAutowireCapableBeanFactory.java:473)
… Еще 59

Это потерпит неудачу … Java не волшебство, поэтому, прежде чем мы продолжим, несколько слов объяснения. Добавление фрагмента XML выше ничего не решает. Это просто говорит Spring, что мы используем AspectJ LTW. Но когда приложение запускается, оно не находит агента AspectJ и сообщает нам об этом прилично. Что произойдет, если мы добавим -javaagent: spring-agent.jar в параметры командной строки JVM, как это было предложено? Этот Java-агент — просто плагин для JVM, который переопределяет загрузку каждого класса. Когда класс Reservation загружается впервые, агент обнаруживает аннотацию @Configurable и применяет некоторый специальный аспект AspectJ к этому классу.

Чтобы быть более точным: байт-код класса Reservation изменяется, переопределяя все конструкторы и процедуры десериализации. Благодаря этой модификации всякий раз, когда создается новый класс Reservation, кроме обычной инициализации, эти дополнительные подпрограммы, добавленные аспектом Spring, обеспечивают внедрение зависимостей. Так что теперь расширенный класс Reservation поддерживает Spring. Не имеет значения, было ли резервирование создано Hibernate, Struts2 или с использованием нового оператора. Код скрытого аспекта всегда заботится о вызове Spring ApplicationContext и просит его внедрить все зависимости в объект домена. Давайте возьмем это для тест-драйва:

01
02
03
04
05
06
07
08
09
10
11
12
13
@Configurable
@Entity
public class Reservation implements Serializable {
 
  @PersistenceContext
  private transient EntityManager em;
 
  @Transactional
  public void persist() {
      em.persist(this);
  }
//...
}

Это не ошибка — я вставил EntityManger из спецификации JPA непосредственно в объект домена. Я также поместил аннотацию @Transactional над методом persist (). Это невозможно в обычном Spring, но, поскольку мы использовали аннотацию @Configurable и AspectJ LTW, приведенный ниже код полностью действителен и работает, как и ожидалось, с выдачей SQL и фиксацией транзакции в базе данных:

1
2
3
Reservation res = new Reservation()
//...
res.persist()

Конечно, вы также можете внедрить регулярные зависимости (другие компоненты Spring) в ваши доменные объекты. Вы можете выбрать автоматическое подключение ( @Autowire или, что еще лучше, @Resource аннотации) или установить свойства вручную. Последний подход дает вам больше контроля, но вынуждает вас добавить установщик для bean-компонента Spring в объекте домена и определить другой bean-компонент, соответствующий объекту домена:

1
2
3
<bean class=" com.blogspot.nurkiewicz.reservations.Reservation ">
  <!-- ... -->
</bean>

Обратите внимание, что я не предоставил имя / идентификатор для этого компонента. Если бы я это сделал, то же имя должно быть передано аннотации @Configurable.

Все работает как шарм, но как мы используем эту удивительную функцию в нашей реальной жизни? Прежде всего, мы должны настроить ваши модульные тесты на использование агента Java. В IntelliJ IDEA я просто добавил:

-javaagent: D: /my/maven/repository/org/springframework/spring-agent/2.5.6/spring-agent-2.5.6.jar

в текстовое поле параметров виртуальной машины в конфигурации запуска JUnit. Если вы добавите его к значению по умолчанию (кнопка «Изменить значения по умолчанию»), этот параметр будет применяться к каждому новому выполненному вами тесту. Но настройка вашей IDE не так важна, как настройка вашего инструмента сборки (надеюсь, maven). Прежде всего вы должны убедиться, что Java-агент Spring загружен и доступен. Благодаря разрешению зависимостей Maven этого легко достичь, добавив следующую зависимость:

1
2
3
4
5
6
<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-agent</artifactId>
      <version>2.5.6</version>
      <scope>test</scope>
</dependency>

JAR на самом деле не нужен тестовому коду, но, добавив эту зависимость, мы гарантируем, что он будет загружен до запуска тестов. Затем, простая настройка конфигурации плагина:

01
02
03
04
05
06
07
08
09
10
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
      <forkMode>always</forkMode>
      <argLine>
          -javaagent:${settings.localRepository}/org/springframework/spring-agent/2.5.6/spring-agent-2.5.6.jar
      </argLine>
  </configuration>
</plugin>

Действительно просто — местоположение spring-agent.jar может быть безопасно построено, используя путь к репозиторию maven. Также должен быть установлен forkMode, чтобы перезагрузить классы (и заставить LTW происходить) перед выполнением каждого теста. Я думаю, что настройка вашего сервера приложений и / или сценариев запуска не нуждается в дополнительном объяснении.

Это все об интеграции Spring и AspectJ через ткачество времени загрузки. Появляется несколько простых шагов настройки и появляется совершенно новый мир доменного дизайна. Наша модель предметной области больше не является слабой, сущности являются «умными», а бизнес-код более интуитивным. И последнее, но не менее важное: ваш код будет объектно-ориентированным, а не процедурным.

Конечно, вам может не понравиться ткачество во время загрузки, поскольку оно мешает загрузке классов JVM. Существует другой подход, называемый ткачеством во время компиляции, который связывает аспекты во время компиляции, а не во время загрузки класса. Оба метода имеют свои плюсы и минусы, я постараюсь сравнить их оба в будущем.

Действительно, очень интересный подход. Вот и все, ребята, компактное руководство о том, как внедрить зависимости в ваши доменные объекты, используя ткачество Spring и AspectJ во время загрузки нашего нашего партнера JCG Томаша Нуркевича . Если вам понравилось это, не забудьте поделиться. Удачного кодирования!

Статьи по Теме: