Статьи

Apache MyFaces — Антипаттерны и подводные камни

Ниже приводится отрывок главы из книги Apress «Полное руководство по Apache MyFaces и Facelets» . Вы можете скачать главу здесь .

[img_assist | nid = 6147 | title = | desc = | link = none | align = right | width = 100 | height = 91] Деннис Бирн работает в ThoughtWorks, глобальном консультанте с упором на сквозную гибкую разработку программного обеспечения. критически важных систем. Он является коммиттером и членом PMC для проекта Apache Myfaces. Он также является коммиттером JBoss JSFUnit и соавтором «Полного руководства по Apache MyFaces и Facelets».

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

N Plus One

Антипаттерн N Plus One обычно попадает в веб-приложения в следующем сценарии:

вы хотите отобразить веб-страницу заказов на покупку вместе с некоторыми данными о клиенте каждого заказа. Эти данные должны быть прочитаны из базы данных. Эффективный подход заключается в извлечении одного набора данных путем объединения таблицы Customer и таблицы Order. Гораздо менее эффективный подход заключается в том, чтобы прочитать набор данных из таблицы «Заказ», выполнить итерацию по этому набору данных и вернуться в базу данных для получения подробной информации о клиенте, связанной с этим заказом. Первый подход стоит одну поездку туда и обратно; второй подход стоит N плюс один туда-обратно, где N — количество заказов. Давайте посмотрим, как этот антипаттерн может найти применение в приложениях JSF.

Мощный шаблон Open Transaction in View приобрел популярность среди разработчиков приложений, использующих каркасы объектно-реляционного отображения (ORM). В сообществе Hibernate шаблон часто носит немного другое имя — Open Session in View. Этот шаблон начинается с открытия транзакции в фильтре сервлетов по мере поступления запроса и закрытия транзакции перед отправкой ответа. Класс OpenTransactionInViewFilter (OTVF) является реализацией этого шаблона:

public class OpenTransactionInViewFilter implements Filter {
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain) {
try {
ObjectRelationalUtility.startTransaction();
chain.doFilter(request, response);
// commits transaction, if open
ObjectRelationalUtility.commitTransaction();
} catch (Throwable throwable) {
try {
ObjectRelationalUtility.rollbackå
Transaction();
} catch (Throwable _throwable) {
/* sans error handling */
}
}
}
public void init(FilterConfig config) throws ServletException { }
public void destroy() { }
}

Прелесть этого шаблона заключается в удобстве запроса структуры ORM в методе действия, помещения постоянного объекта в область запроса и разрешения обработчику навигации пересылать запрос в соответствующий шаблон представления. Разработчик страницы может получить данные в ответ через выражения JSF EL. Данные могут быть лениво загружены при визуализации страницы, и каждое выражение EL JSF обходит граф объекта в области запросов.

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

<!-- One trip to the database for the projects ... -->
<h:dataTable value="#{projectBean.projects}" var="project">
<h:column>
<h:commandLink action="#{projectBean.viewProject}"
value="view project"/>
</h:column>
<h:column>
<!-- ... and plus N trips for each project manager record -->
<f:facet name="header">Project Manager</f:facet>
#{project.manager.name}
</h:column>
<h:column>
<f:facet name="header">Project Name</f:facet>
#{project.name}
</h:column>
<h:column>
<f:facet name="header">Start Date</f:facet>
#{project.startDate}
</h:column>
<h:column>
<f:facet name="header">End Date</f:facet>
#{project.endDate}
</h:column>
</h:dataTable>

Данные для этой формы могут быть получены за одну поездку в базу данных. Вместо этого один постоянный экземпляр модели предметной области передавался в шаблон представления, и JSF EL Resolver инициировал дополнительное отключение в базе данных для каждой отображаемой строки. Методы действия в управляемых bean-компонентах недостаточно глубоко копались в базе данных и вызывали известный антипаттерн N Plus One.

Было принято архитектурное решение изолировать все транзакции базы данных на этапе вызова приложения с помощью класса OpenTransactionInApplicationPhaseListener (OTAPL), который выполняет все, что предоставляет OTVF, с более точным разграничением транзакций. OTVF ограничивает транзакцию жизненным циклом запроса, в то время как OTAPL ограничивает транзакцию одной фазой жизненного цикла запроса. Это тонко определяет разницу между веб-страницей, отображаемой в O (n), и веб-страницей, отображаемой в O (1).

public class OpenTransactionInApplicationPhaseListener
implements PhaseListener {
public void beforePhase(PhaseEvent event) {
try {
ObjectRelationalUtility.startTransaction();
} catch (Throwable throwable) {
/* sans error handling */
}
}
public void afterPhase(PhaseEvent event) {
try {
// commits transaction, if open
ObjectRelationalUtility.commitTransaction();
} catch (Throwable throwable) {
try {
ObjectRelationalUtility.rollbackå
Transaction();
} catch (Throwable _throwable) {
/* sans error handling */
}
/* sans error handling */
}
}
public PhaseId getPhaseId() {
return PhaseId.INVOKE_APPLICATION;
}
}

 

OTVF был заменен на OTAPL, и испытания были проведены. Шаблоны представления, запускающие операции чтения в базе данных, можно было бы скрыть, закрыв транзакцию перед фазой рендеринга ответа. Страницы с исключениями можно использовать для отслеживания того, какие объекты доступа к данным необходимо настроить. Каждый модульный тест пройден; более половины интеграционных тестов не удалось.
Этот простой PhaseListener не только выделил проблему производительности, но и повысил осведомленность о риске. В жизненном цикле обработки запросов есть особое время для риска: фаза вызова приложения. Большое количество JSF-приложений подвергают риску на более позднем этапе жизненного цикла, на этапе рендеринга ответов, выполняя большинство отключений базы данных после фазы действия во время рендеринга страницы, без возможности восстановления после неожиданного. Это худшее время, чтобы сделать что-то рискованное; даже параметры регистрации ограничены. Такое поведение противоречит одной из целей паттерна MVC: держать представление обеспокоенным отображением. Попадание в базу данных во время фазы рендеринга ответа снижает ценность ваших блоков try / catch в методах действия. Не могли бы вы написать такой метод действий?

public String doAction() {
String outcome = "success";
try {
getDao().goToTheDataBase();
}catch(Exception e) {
handleException();
outcome = "failure";
}finally {
// Exception uncaught
getDao().goToTheDataBaseAgain();
}
return outcome;
}

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

В жизненном цикле обработки запросов JSF есть время и место для риска: фаза вызова приложения. Этот этап состоит из двух частей: слушатели действий и действия. Любой источник действия может иметь ноль или более слушателей действия и один метод действия. Сначала всегда вызываются слушатели действий, а затем метод действия. Слушатели действий и методы действий имеют различные свойства и компромиссы. Слушатели действий предоставляют разработчику приложения ссылку на ActionEvent. ActionEvent предоставляет полезную информацию, такую ​​как ссылка на компонент ActionSource, откуда произошло событие. Эта информация не так легко доступна в методе действия, где нет ссылки на ActionEvent. Методы действия, с другой стороны, отличаются от слушателей действия, потому что каждый из них имеет тип возврата,что-то, на что может реагировать обработчик навигации.

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

 

Если вам нравится этот отрывок из книги Apress «Полное руководство по Apache MyFaces и Facelets» . Вы можете скачать главу здесь .

Трюк с картой

Антипаттерн Map Trick — это хак, используемый для вызова бизнес-методов из шаблонов представлений посредством неясного ограничения в спецификациях JSP и JSF. Вызывая бизнес-методы из шаблона, представление и модель становятся тесно связанными.
JSF EL и Unified EL не поддерживают параметризованный вызов метода. Вы получаете троичную логику и можете вызывать геттеры, но это все. Разработчики гобеленов или кто-либо еще, знакомый с языком выражений Object-Graph Navigation Language (OGNL), часто разочаровывается, узнав это, потому что OGNL поддерживает параметризованный вызов метода. Наиболее близким к параметризованному вызову метода является статический вызов метода через JSP EL или Facelets.
Интерфейс Map является единственным исключением из этого правила. JSP EL, JSF EL и Unified EL поддерживают вызов метода get, параметризованного метода интерфейса Map:

# {myManagedBean.silvert} // извлекает ‘silvert’ из управляемого компонента bean Map
# {param [‘lubke’]} // извлекает параметр запроса ‘lubke’

Некоторые разработчики реализовали свою собственную карту, чтобы воспользоваться этим.

public class MapTrick implements Map {

public Object get(Object key) {
return new BusinessLogic().doSomething(key);
}
public void clear() { }
public boolean containsKey(Object arg) { return false; }
public boolean containsValue(Object arg) { return false; }
public Set entrySet() {return null; }
public boolean isEmpty() { return false; }
public Set keySet() { return null; }
public Object put(Object key, Object value) { return null; }
public void putAll(Map arg) { }
public Object remove(Object arg) { return null; }
public int size() { return 0; }
public Collection values() { return null; }
}

Когда EL Resolver вызывает метод get, параметр используется бизнес-логикой. Однажды мы увидели проект, в котором весь миниатюрный каркас был построен вокруг уловки карты. Излишне говорить, что вид и модель были сильно связаны между собой. Всегда есть лучшие альтернативы трюку с использованием простых методов и выражений значений.

Déjà Vu PhaseListener

Жизненный цикл запроса JSF разбит на фазы. Начало и конец каждой фазы считается событием, и на эти события можно подписаться через PhaseListener. Если PhaseListener подписывается на фазу представления восстановления жизненного цикла запроса, MyFaces будет вызывать метод обратного вызова для экземпляра PhaseListener каждый раз, когда фаза представления восстановления начинается и заканчивается для запроса. Когда методы обратного вызова PhaseListener вызываются дважды, это называется PhaseListener Déjà Vu, старая проблема, которая часто возникает в списке рассылки MyFaces.

Совет
Один из самых активных списков рассылки для Apache Software Foundation — [email protected] . Это прекрасное место для обмена новыми идеями, обмена техническими решениями и участия в пламенных войнах. После всех этих лет MyFaces остается проектом с открытым исходным кодом, в котором команда разработчиков по-прежнему взаимодействует с разработчиками приложений.

Давайте посмотрим, как PhaseListeners регистрируются во время запуска. PhaseListeners можно зарегистрировать в любом файле конфигурации JSF:

<lifecycle>
<phase-listener>
org.apache.myfaces.PhaseListenerImpl
</phase-listener>
</lifecycle>
JSF configuration files are specified via the javax.faces.CONFIG_FILES context parameter
in the deployment descriptor:
<context-param>
<description>comma separated list of JSF conf files</description>
<param-name>javax.faces.CONFIG_FILES</param-name>
<param-value>
/WEB-INF/faces-config.xml,/WEB-INF/burns.xml
</param-value>
</context-param>

Согласно спецификации JSF, MyFaces автоматически анализирует /WEB-INF/faces-config.xml, а также все файлы, указанные в списке через запятую параметра контекста javax.faces.CONFIG_FILES. Когда /WEB-INF/faces-config.xml указан в параметре контекста javax.faces.CONFIG_FILES, он анализируется дважды. PhaseListeners, сконфигурированные в этом файле, следовательно, дважды регистрируются при запуске и запускаются дважды во время выполнения. MyFaces пытается предупредить вас об этом в файлах журнала:

WARN org.apache.myfaces.config.FacesConfigurator — /WEB-INF/faces-config.xml был указан в параметре контекста javax.faces.CONFIG_FILES дескриптора развертывания. Это должно быть удалено, так как оно будет загружено дважды. См. JSF spec 1.2, 10.1.3

XML Hell ++

JSF 1.1 и 1.2 совместимы с Java 1.4. Это требование ограничивает спецификацию JSF от использования аннотаций Java для объявления таких вещей, как правила навигации или внедрение зависимостей. Эти вещи объявлены в XML вместо этого. Однажды мы рассмотрели проект в разработке с файлом конфигурации, подобным следующему.
Страница «Связаться с нами» должна была быть доступна практически со всех страниц сайта, и для каждой страницы использовалось отдельное правило действий.

<navigation-rule>
<from-view-id>/home.xhtml</from-view-id>
<navigation-case>
<from-outcome>contact_us</from-outcome>
<to-view-id>/contact.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<from-view-id>/site_map.xhtml</from-view-id>
<navigation-case>
<from-outcome>contact_us</from-outcome>
<to-view-id>/contact.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<navigation-rule>
<from-view-id>/about_us.xhtml</from-view-id>
<navigation-case>
<from-outcome>contact_us</from-outcome>
<to-view-id>/contact.xhtml</to-view-id>
</navigation-case>
</navigation-rule>
<!-- continued ... -->

Правило глобальной навигации использовалось для уменьшения файла конфигурации более чем на сто строк:

<navigation-rule>
<from-view-id>*</from-view-id>
<navigation-case>
<from-outcome>contact_us</from-outcome>
<to-view-id>/contact.xhtml</to-view-id>
</navigation-case>
</navigation-rule>

 

Если вам нравится этот отрывок из книги Apress «Полное руководство по Apache MyFaces и Facelets» . Вы можете скачать главу здесь .