Abstract
В этой статье описывается структура, которая позволяет создавать полностью автоматизированные бизнес-веб-приложения.
Проект
Home Counselor Online - это система управления взаимоотношениями с клиентами, используемая консультантами по жилищным вопросам. FannieMae предлагает ее как услугу в основном некоммерческим организациям. См. подробную Презентацию функциональности HCO. [ДОПОЛНЕНИЕ 31.10.2019: HCO обслуживала тысячи домашних консультантов в Соединенных Штатах в течение 18 лет, пока не была заменена на независимый сервис. Fannie Mae закрывает Home Counselor Online ]
Задача
Я присоединился к команде HCO в 2001 году, в самом начале проекта. Я предложил руководителю проекта, что я для начала построю каркас для приложения: у него будет экран входа в систему, предварительная версия домашней страницы, и пара страниц с примерами и заглушками, которые другие разработчики смогут заполнять функциональностью. Это сработало, и через несколько месяцев мы выпустили первую версию HCO. Она была основана на Struts 1.0, JSP, Weblogic, имела несколько EJB и использовала TOPLink для сохранения данных в Sybase.
В течение следующих двух лет мы продолжали расширять приложение, добавляя все больше и больше функций.
Проблема
За эти два года, как и ожидалось, приложение начало выходить из-под контроля: разработчики предлагали отличающиеся решения схожих проблем, экраны начали отклоняться от стандартного внешнего вида, количество кода на Java и JSP росло.
Так совпало, что в этот момент заказчики приложения решили его полностью обновить. Нам дали совершенно новую визуальную парадигму, разработанная для нас фирмой CDG Interactive. Дизайн, предложенный CDG и принятый заказчиками HCO, не имел ничего общего с нашим текущим дизайном. Нам предстояло переписать весь интерфейс приложения. Это дало нам фантастическую возможность улучшить общий дизайн приложения.
Решение
Я предложил своему менеджеру разработать платформу, которая автоматизировал бы дизайн, созданный CDG. К счастью, их дизайн был отличного качества и придерживался внутреннего стандрта на всех страницах.
CDG использовала шаблоны проектирования, например тот, который я назвал редактируемый список. В этом шаблоне, в верхней части экрана отображается список каких-либо объектов (адреса, активы, обязательства, встречи и т. д.). Когда вы щелкаете по одной из этих элементов, на экране появляется редактируемая панель деталей. Также было несколько других шаблонов.
Мое предложение состояло в том, чтобы создать автоматическую поддержку этих шаблонов и упрвавлять их реализацией с помощью конфигурации. Конфигурация была бы выражена в основном в специализированых тегах JSP. Нам не пришлось бы писать много кода, будь то Java или HTML, для каждого отдельного экрана: вместо этого мы бы по-разному настраивали автоматический шаблон. Автоматизация позаботится как о внешнем виде, так и о реализации серверной части. Я преставил эти идеи в презентации и показал ее руководителям проекта. Я утверждал, что мы перепишем все приложение за 4 месяца и одновременно уменьшим его размер. Они сказали да.
Я разработал среду программирования под названием DA (Другая архитектура), состоявшую из контроллеров, форм, генераторов HTML и пользовательских тегов JSP. Среда реализовывала шаблоны CDG, а также все рутинные задачи, включая обработку транзакций, перемещение данных между уровнями, проверку пользовательского ввода, обработку безопасности и, самое главное, генерацию почти всего HTML. Осталось только настроить и отшлифовать отдельные экраны. План сработал великолепно, и версия 4 была выпущена вовремя, как и было обещано.
Рассмотрим пример. Нам нужен был экран с редактируемым списком:
Для реализации этого экрана мы создали всего два артефакта: JSP и класс FormBean:
<%@ taglib uri="da.tld" prefix="da" %> <da:editableList formBean="Assets" title="Assets" emptyListMessage="No Asset information recorded."> <da:action type="add" label="Add Asset" /> <da:rowAction type="edit" /> <da:rowAction type="delete" /> <da:column property="depositoryName" /> <da:column property="assetTypeCode" /> <da:column property="assetValueAmount" label="Asset Value" footerProperty="totalAssetValue" /> <da:column property="availableFundsAmount" label="Available Funds" footerProperty="totalAvailableFunds" /> <da:footer align="right"> Total: </da:footer> <da:detail title="Asset Information Details"> <da:input property="assetTypeCode" width="250px" /> <da:input property="depositoryName" width="250px" /> <da:input property="assetAccountNumber" width="250px" /> <da:input property="assetValueAmount" /> <da:input property="availableFundsAmount" /> <da:action type="save" /> <da:action type="cancel" /> </da:detail> </da:editableList>
Далее следует реализация серверной части на Java. Упомянутые здесь классы CaseFile и Asset представляют собой обычные JavaBeans без бизнес-логики и с небольшим количеством аннотаций для декларации типов полей, названий, диапазонов и т. д., когда они отличаются от значений по умолчанию. Поля сохраняются в базе данных с помощью TOPLink.
package com.fanniemae.xo.web.casefile;
public class AssetEditableList extends EditableListForCaseFileElements
{
public AssetEditableList(CaseFileController controller) {
super(controller, Asset.class);
}
public List getList() {
return getCaseFile().getSortedAssets();
}
protected void saveNewBean(Asset bean) {
getCaseFile().addAsset(bean);
}
protected void deleteBean(int index) {
Asset asset = getRow(index);
getCaseFile().removeAsset(asset);
}
public void validate(
HttpServletRequest request,
ActionDescriptor action)
{
// Note: no format, range or other types of validations here
Asset asset = getDomainObject();
if(asset.getAssetValueAmount() < asset.getAvailableFundsAmount()) {
addValidationError("Available Funds amount "
+ "cannot be greater than the Asset Value.");
}
}
@Type(Type.MONEY)
public double getTotalAssetValue() {
return getCaseFile().getTotalAssetValueAmount();
}
@Type(Type.MONEY)
public double getTotalAvailableFunds() {
return getCaseFile().getTotalAvailableFundsAmount();
}
public String save(
HttpServletRequest request,
HttpServletResponse response)
{
getCaseFile().updateAssetTotals();
return super.save(request, response);
}
}
В 2005 году я расширил среду возможностями позволяющими таким же образом генерировать отчеты. Вы просто описываете содержимое отчета, а среда автоматически позаботится обо всем форматировании и т. д. См. определение отчета и образец отчета.
Я ожидал, что другие приложения можно будет реализовать на том-же подходе, поэтому я сделал всю среду на основе скина. Это означает, что конкретный вид экранов и отчетов определялся набором классов, которые можно было полностью или частично заменить. И, конечно же, появилось еще одно приложение. Мы создали новую оболочку, основанную на той, что используется в HCO, но несколько отличающуюся, и написали другое приложение таким же образом, используя структуру DA.
Были допущены ошибки
When Jack Tran interviewed me for this job, the entire interview lasted just a few minutes. He showed me a draft specification of the system and asked: "Can you write an app like this?" I gave him a confident yes. The reality is that I had very little idea of how to write a web app. From my previous job at Inline Software, I had learned a specific set of solutions, such as EJB, which were actively pushed by Sun Microsystems at the time. It took years for me to realize that EJB just added an entirely unnecessary layer of complexity. It's much easier and more efficient for servlets to talk to the database directly.
The template-based design was a clear success. At the same there was one specific aspect of that project that was an embarrassing epic fail. I designed the application to be stateful: there was a server-side HttpSession maintained for every user. This design produced a variety of problems:
- Приложение не могло масштабироваться по мере роста количества пользователей: каждый пользовательский сеанс требовал определенного количества ОЗУ, занятого на все время сеанса. Fannie Mae владела своими серверами, очень ограниченным их количеством, так что память была на счету.
- ВАЖНО: мы использовали глобальный балансировщик нагрузки. Балансировщик нагрузки случайным образом направляет трафик на разные серверы. Пользователь открывал сеанс и начинал работать. Через 40 минут разрешение DNS для сервера истекало в браузере, браузер обращался к DNS-серверу для разрешания домена. На DNS-сервере находился балансировщик нагрузки. Он возвращал IP-адрес случайно выбранного сервера. Таким образом, следующий HTTP-запрос от браузера в большинстве случаев приходил на другой сервер. На этом новом сервере не было сеанса пользователя, поэтому он откланял запрос как не прошедший проверку подлинности. Пользователь должен был снова войти в систему, а затем повторить последний ввод данных.
В то время я «решил» проблему, включив идентификатор сеанса в каждое сообщение. Когда сервер получает запрос, он сначала анализирует идентификатор. Идентификатор содержит адрес сервера, на котором размещен сеанс. Сервер, получивший запрос, переадресует его серверу, на котором находится сеанс. Это «решение» работало, но имело два фундаментальных недостатка:
- Балансировка нагрузки была нарушена. Вместо того, чтобы равномерно распределять нагрузку на серверы, теперь все серверы выполняли дополнительную работу по пересылке запросов друг другу.
- ВАЖНО: если один из серверов выйдет из строя, другие серверы все равно будут пытаться пересылать ему запросы. В результате на этих других серверах будет наблюдаться увеличение количества сбоев, и они будут перезапущены автоматизацией. В результате один отказавший сервер отключает всё веб-приложение для всех пользователей за считанные минуты. Это произожло на самом деле несколько раз. С помощью небольшого поправки мы «устранили» проблему: мы научились обнаруживать этот тип сбоя и восстанавливать функциональность системы.
Извлеченный урок: серверы с балансировкой нагрузки не должны иметь зависимостей друг от друга. Поэтому приложение не должно использовать данные хранимые в памяти сервера. Все пользовательское состояние должно либо храниться в базе данных, либо пересылаться туда и обратно в каждом запросе.