суббота, 16 августа 2014 г.

Опыт реального использования практически всех возможностей Java EE 6

Суровый конечно же понимает, что данная статья несколько запоздала, ей бы самое время появиться году так в 2010-м. Однако, я не привык писать о том, с чем не поработал сколь-либо плотно, а разработать приложение, опирающееся практически на весь стек Java EE 6 мне удалось только сейчас. В данной заметке собраны впечатления об использовании данных технологий, полученные за время ведения проекта разработки интеграционного приложения, обеспечивающего асинхронное взаимодействие нескольких информационных систем. Я не имею права подробно описывать архитектуру и принятые дизайнерские решения, поэтому изложение будет несколько лоскутным: задача, как решали, впечатления.

Модульность и развертывание


Начну разговор с рассказа о наиболее общем, т.е. о сборке приложения и его развертывании. Сборка приложения осуществляется с помощью Apache Maven, в качестве среды разработки, тестирования и промышленной эксплуатации используется сервер приложений Oracle WebLogic 12c (12.1.2). Многие разработчики и я - не исключение, поистине любят Apache Maven за его декларативность, модульность и интеграцию с Java EE, что называется из коробки. Нужно лишь указать правильный тип собираемого артефакта (packaging) и любой тип Java EE-модуля будет собран, будь то EAR, WAR, EJB-JAR или обычный всем нам хорошо знакомый JAR с какой-нибудь библиотекой. Однако при необходимости, процесс сборки можно кастомизировать, подключив и настроив соответствующий плагин.

Спецификация Java EE 6 привнесла такие новшества, как возможность полностью отказаться от дескрипторов развертывания. Сервлеты теперь можно определять с помощью аннотаций, что в большинстве случаев сводит на нет необходимость в файле web.xml. Чтобы Apache Maven игнорировал отсутствие данного файла, необходимо правильно настроить плагин maven-war-plugin:


<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <version>2.4</version>
    <configuration>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </configuration>
</plugin>

Стоит отметить, что данных настроек достаточно, даже если в WEB-модуле находятся EJB-компоненты, дополнительно настраивать плагин maven-ejb-plugin нет необходимости.

Отдельно стоит поговорить о модульности. В принципе, Java EE изначально разрабатывалась как модульная технология. В Java EE 6 система модулей была существенно улучшена: теперь нет необходимости выносить EJB-компоненты в отдельный модуль ejb-jar. Небольшое приложение, состоящее из веб- и EJB-частей может целиком разворачиваться как один WAR-архив. Если же приложение побольше, то все равно применение комбинированных WEB-EJB модулей существенно упростит жизнь системного администратора.

Развертывание на сервере приложений Oracle WebLogic 12c так же осуществляется посредством Apache Maven. Начиная с 12-й версии у сервера приложений появился довольно удобный Maven-плагин, позволяющий как управлять жизненным циклом сервера и домена, так и осуществлять развертывание, запуск, останов и обновление приложений.

Впечатления: в рассматриваемом случае было разработано восемь модулей, собираемых в два EAR-архива: один архив содержит приложение, собственно осуществляющее интеграцию, другой - управляющее приложение с веб-интерфейсом. Данное решение позволяет обновлять основную часть, не затрагивая веб-интерфейс, с которым в момент обновления могут работать люди. В случае необходимости вынесения EJB в отдельные модули работы было бы больше.

Организация бизнес-логики


IoC-контейнер

Одним из самых радикальных нововведений в Java EE 6 стало появление мощного IoC-контейнера под названием Contexts and Dependency Injection (CDI). Если в Java EE 5 появилась только лишь возможность инъектировать EJB в сервлеты и другие EJB, то сейчас ситуация существенно изменилась. Посредством CDI могут инъектироваться как все управляемые компоненты (EJB и Managed Beans), так и ресурсы (например, Data Sources) и даже обычные классы. Настройка инъектирования больше похожа на Google Guice, нежели на более привычный Spring Framework, который хоть уже давно и поддерживает аннотации, но многие вещи, особенно требующие доступа к ресурсам, таким как источники данных, по прежнему удобнее и проще настраивать через XML.

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


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.TYPE, ElementType.METHOD, ElementType.FIELD,
        ElementType.PARAMETER })
public @interface OneLoader {
}


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.inject.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.TYPE, ElementType.METHOD, ElementType.FIELD,
        ElementType.PARAMETER })
public @interface TwoLoader {
}

Классы, объекты которых будут инъектироваться:


@OneLoader
public class OneLoadQuery extends AbstractMappedLoadQuery<One> {
   ...
}


@TwoLoader
public class TwoLoadQuery extends AbstractMappedLoadQuery<Two> {
   ...
}

Определение точек инъекции:


@Stateless
public class LoaderService {

    @Inject @OneLoader
    private SomeLoader oneLoader;

    @Inject @TwoLoader
    private SomeLoader twoLoader;
...
}

Другая решаемая посредством CDI проблема - доступ к ресурсам. Например, в приложении необходимо построить реализацию паттерна DAO, в которую поместить ссылку на источник данных. Реализуется такая инъекция с помощью специальных методов - продюсеров, аннотированных @javax.enterprise.inject.Produces:


import javax.annotation.Resource;

import javax.enterprise.context.ApplicationScoped;

import javax.enterprise.inject.Produces;

import javax.sql.DataSource;

import org.apache.log4j.Logger;

...

public class DaoConfiguration {

    private static final Logger logger =
        Logger.getLogger(DaoConfiguration.class);
    
    private static final String DAO_CREATION_ERR = "...";
    
    public DaoConfiguration() {
        super();        
    }    

    @Resource(name = "jdbc/datasource")
    private DataSource dataSource;

    @Produces @ApplicationScoped
    public GenericCallableDao getCallableDao() {
        try {
            return new OracleGenericCallableDao(dataSource);
        } catch (DaoConfigurationException e) {
            logger.error(DAO_CREATION_ERR, e);
            throw new ConfigurationException(DAO_CREATION_ERR, e);
        }
    }
}

В рассматриваемом приложении использование CDI позволило реализовать такие затратные по числу классов и инъекций паттерны, как Query Object, а так же избавило от необходимости делать каждый класс EJB-компонентом. Более того, рекомендуется даже управляемые компоненты JSF определять через CDI (с помощью аннотации @javax.inject.Named), а сами EJB инъектировать с помощью аннотации @javax.inject.Inject, а не @javax.ejb.EJB.

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

Впечатления: в целом очень даже положительные. Но заметен ад аннотаций, их действительно приходится писать слишком много. В рассматриваемом случае их число удалось бы сильно сократить, если бы CDI мог определять инъекцию не только по типу самого поля, но и по типу дженерика для данного поля аналогично тому, как это делает Spring Framework. Так же следует отметить, что для CDI-компонентов не поддерживаются декларативные настройки безопасности. Такие настройки доступны только для EJB.

Управление транзакциями

Управление транзакциями осуществляется посредством EJB-компонентов. В приложении используются управляемые контейнерами транзакции (CMT), демаркация границ которых осуществляется посредством стандартных аннотаций @javax.ejb.TransactionAttribute. В принципе, данные настройки появились еще в Java EE 5. Существенные изменения в управление транзакциями внесены в спецификации Java EE 7: теперь контейнер может оборачивать в транзакции и методы CDI-компонентов, что во многих случаях позволяет полностью отказаться от использования EJB.

Запускаемые при старте приложения компоненты

Инъекция зависимостей как в EJB-, так и в CDI-компоненты ленивая, т.е. инъектируемый объект будет построен только если выполняется построение зависящего от него компонента. Чтобы построить, например, кэш каких-либо данных, необходимо, чтобы из некоторой точки программы было обращение к содержащему данный кэш объекту. В Java EE 6 ситуация изменилась: чтобы компонент был построен во время запуска приложения, его необходимо аннотировать @javax.ejb.Startup. При этом будет выполнен метод компонента, аннотированный @javax.annotation.PostConstruct. В данном методе можно загрузить объекты в кэш, запустить таймеры или выполнить другие действия.

Если необходимо, чтобы компонент был построен после успешного запуска какого-либо другого компонента, то его необходимо аннотировать @javax.ejb.DependsOn, указав в качестве параметра аннотации наименование интересующего компонента.


@Singleton
@Startup
@DependsOn("ConfigProviderTimerBean")
public class LoadDataTimerBean {

    @PostConstruct
    private void initTimers() {
       // ... логика запуска компонента
    }

    @PreDestroy
    private void removeTimers() {
       // ... логика останова компонента
    }
}

К сожалению автоматически запускаемыми могут быть только т.н. Singleton Session Beans - новый тип EJB-компонентов, появившийся в Java EE 6.

Singleton Session Beans

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

Singleton Session Bean работает аналогично сервлету: создается один и только один экземпляр компонента. При этом по-умолчанию доступ ко всем бизнес-методам такого экземпляра организуется строго последовательно, т.е. два клиента не могут одновременно вызвать его метод.

Управляется данное поведение с помощью аннотации @javax.ejb.Lock. Данная аннотация принимает на вход значение из перечисления javax.ejb.LockType. По-умолчанию данное значение равно javax.ejb.LockType.WRITE, что приводит к организации последовательного доступа к методам. Если в каком-либо методе не производится изменение состояния общих объектов и он может выполняться в многопоточной среде, то его можно аннотировать @Lock(LockType.READ), тогда будет разрешено одновременное выполнение метода из нескольких потоков.

Впечатления: в рассматриваемом приложении автоматически запускаемые синглетные компоненты используются для управления таймерами, отвечающими за загрузку данных по расписанию. Аннотация @DependsOn используется для указания порядка запуска компонентов: сначала считывающий и периодически обновляющий конфигурацию приложения, затем - выполняющий основную работу. Так как таймеров создается несколько и все используют один и тот же метод timeout, то пришлось разрешить многопоточное исполнение данного метода с помощью аннотации @Lock.


@Singleton
@Startup
@DependsOn("ConfigProviderTimerBean")
public class LoadDataTimerBean {

    @Resource
    private TimerService timerService;

    @PostConstruct
    private void initTimers() {
       // ... логика запуска компонента
    }

    @PreDestroy
    private void removeTimers() {
       // ... логика останова компонента
    }

    @Timeout
    @Lock(LockType.READ)
    private void timeout(Timer timer) {        
        ...
    }
}

Новое представление EJB-компонентов: без интерфейса

В Java EE 6 к двум существовавшим ранее представлениям EJB-компонентов: удаленному и локальному интерфейсам, добавилось третье: представление компонента без интерфейса. Суть в следующем: теперь EJB компонент может не иметь ни локального, ни удаленного интерфейсов. В точках инъекции EJB-компонентов можно использовать сразу имя конкретного класса.


@Stateless
public class MyBean {
    ...
}

@Stateless
public class MyAnotherBean {
   
    @Inject
    private MyBean mybean;

    ...
}

Хотя лучше конечно так не делать - не определять поля, тип которых - конкретный класс. Инъекцию лучше осуществлять посредством интерфейсов. Иначе будет очень сложно подменить реализацию при необходимости. Полезно же безынтерфейсное представление EJB-компонентов в том случае, когда они не предназначены для инъекции. Т.е. представляют собой или автоматически стартуемый синглетный компонент, осуществляющий регистрацию таймеров или выполнение каких-либо других действий по инициализации приложения, либо веб-сервисный фасад. Еще в EJB 2.1 появилась возможность предоставить доступ к компоненту посредством механизма веб-сервисов. Понятно, что иметь интерфейс (в терминах Java) такому компоненту абсолютно незачем. Необходимость поддержки такого интерфейса только лишь добавляет работы разработчикам: при расширении нужно добавлять метод и в интерфейс, и в реализацию, когда на самом деле он нужен только в последней.


@Stateless
@WebService(name = "LoaderService", serviceName = "LoaderService",
    portName = "LoaderServicePort",
    targetNamespace = "http://integration/adapter/system")
public class LoaderService {

    // ...
 
    @WebMethod
    public void loadOneType(@WebParam(name = "branchId") long branchId,
            @WebParam(name = "count") int count) throws DataLoadException {
        // ...
    }

    @WebMethod
    public void loadTwoType(@WebParam(name = "branchId") long branchId,
            @WebParam(name = "count") int count) throws DataLoadException {
        // ...
    }

    // ...
}

Работа с настройками приложения

Пункт, для реализации которого в Java EE 6 нет ничего. Стандартного API, кроме предназначенного для работы с .properties-файлами, не существует. Но одними .properties-файлами сыт не будешь. В рассматриваемом примере было реализовано сохранение настроек приложения в базе данных в виде сериализуемого в CLOB XML. Работа с XML ведется с помощью JAXB. В принципе, применять для хранения настроек базу данных довольно избыточно, но с другой стороны спецификация не приветствует работу с файловой системой сервера. На мой взгляд обобщенного хорошего API для работы с конфигурацией приложения платформе очень не хватает.

Разработка веб-интерфейса


JSF 2

Существенным прорывом в Java EE 6 стала спецификация JSF 2, описывающая MVC-фреймворк для разработки веб-интерфейса приложения. Нововведений по сравнению с предыдущей версией очень много. Прежде всего это конечно новый механизм описания разметки - фейслеты (facelets). Прощай, прощай, JSP, ты нас утомил. В фейслетах улучшена интеграция с JSTL, которая была довольно таки кривая в JSF 1.2. Радует, что теперь из разметки можно явно вызвать метод управляемого компонента и передать ему параметры, иногда это необходимо. Так же очень облегчает жизнь применение шаблонов, теперь страница строится путем встраивания специфичных фрагментов в обобщенный шаблон, что помогает обеспечить единый вид для всего приложения. В принципе нет смысла пересказывать здесь Java EE 6 Tutorial, лучше расскажу про то, что не понравилось.

Впечатления: несмотря на поразительный шаг вперед по сравнению с предыдущими версиями, некоторые моменты портят впечатление. Во-первых, не понравилось, что в одном месте нельзя вывести сообщение об ошибках, произошедших в нескольких компонентах. Либо выводи вообще все сообщения об ошибках на странице, либо все сообщения для одного компонента. Возможности вывести все первые сообщения об ошибках нескольких компонентов в одном месте нет. Во-вторых, в XXI-м веке то можно корректно обрабатывать HTML-код в сособщениях об ошибках (<h:message ... />). Если я в своем messages.properties завожу сообщение об ошибке, содержащее разметку, то в браузере данная разметка отображается как есть, т.е. JSF ее экранирует.

Методы authenticate(), login() и logout() в Servlet API

Нововведение конечно не такое масштабное, как JSF 2, но тоже полезное. Особенно, если вы реализуете аутентификацию в вашем веб-приложении посредством стандартных механизмов Java EE. Теперь можно не только настроить FORM-BASED аутентификацию со специальной страницей, содержащей форму логина, но и встроить данную форму в ваше приложение на нужное место. Например, в верхний правый угол каждой страницы. При этом, при нажатии кнопки login, можно самому обработать процесс аутентификации, например посредством AJAX. Так же теперь (всего-лишь в третьей версии Servlet API!) появился стандартный механизм, позволяющий реализовать выход из приложения.


@WebServlet(name = "LogoutServlet", urlPatterns = { "/logout" })
public class LogoutServlet extends HttpServlet {
    
    @SuppressWarnings("compatibility:6960206080870060286")
    private static final long serialVersionUID = 1L;

    public LogoutServlet() {
        super();
    }
    
    public void doGet(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        try {
            if (request.getUserPrincipal() != null) {    
                request.logout();
            }
            response.sendRedirect(getServletContext().getContextPath());                            
        } finally {
            response.flushBuffer();
        }
    }
}

Выводы


Нельзя не отметить, что с каждой новой версией Java EE становится все более и более удобной для разработки. От монструозного детища с EJB 2.0, когда для запуска компонента необходимо было написать два интерфейса, один класс и дескриптор развертывания, не осталось и следа. Java EE 5 была прорывом, Java EE 6 тоже есть чем похвастаться. Это конечно же гораздо больше, чем недавнее изменение пространств имен в дескрипторах развертывания. Хотя есть еще куда развиваться.

Если перед вами стоит задача выбора технологии для нового проекта и есть возможность использовать качественный промышленный сервер приложений, то посмотрите в сторону Java EE. Здесь есть все для удобной и легкой разработки корпоративного приложения. Может быть не стоит тянуть за собой Spring Framework, увеличивая объем поставляемых с приложением библиотек, а значит и время его развертывания. К тому же завязываться на одного поставщика ПО - не лучшая идея.

Буду благодарен, если вы поделитесь своими впечатлениями от использования Java EE в вашем приложении. Особенно интересны проблемы, с которыми столкнулись на продуктиве. Может быть вы уже во всю запускаете что-то на Java EE 7?

Понравилось сообщение - подпишитесь на блог

11 комментариев:

Sergey Kisel комментирует...

"Инъекцию лучше осуществлять посредством интерфейсов. Иначе будет очень сложно подменить реализацию при необходимости." - если на данный момент реализация всего-лишь одна, какой смысл плодить интерфейсы? в случае необходимости делается рефакторинг, нужные методы выносятся через Pull Members Up в интерфейс и все использования класса меняются на новосозданный интерфейс. Работы - 30 секунд.

Pavel Samolisov комментирует...

Сначала хотел поспорить, но потом подумал, что не стоит. В конце концов вам и только вам решать, какие подходы использовать в своих приложениях. Авторы спецификации EJB 3.1 предоставили возможность выбора и в этом вопросе.

Sergey Kisel комментирует...

Очень жаль, что не спорите. Я этот спор уже провоцировал не один раз и меня пока так никто и не убедил - может быть у вас получилось бы :)

Denis K комментирует...

Павел, спасибо за статью!

Nikita Eshkeev комментирует...

@Sergey Kisel, в своей практике, если я знаю, что так правильно, но не знаю почему, а какой-нибудь член команды говорит, что надо делать наоборот, я соглашаюсь с ним, потом через пару месяцев всплывает проблема, которой не было бы если бы воспользовались тем путем, который предлагал я. В итоге, вес моего слова в команде растет, люди начинают прислушиваться ко мне и репутация тоже растет. Плюс к этому я знаю почему так делать правильно и какие проблемы могут возникнуть, если пойти другим путем.
Например, одна из последних проблем, в и без того большую иерархию ДАО я предлагал добавить еще один уровень для справочников, и вытащить туда общие поля для справочников, но другой член команды сказал, что ДАО дерево итак большое, добавлять не будем, потом через 3 месяца всплыла задача однообразной обработки справочников и вот тут нам пришлось костыль из рефлексии втыкать, хотя можно было красиво обойтись дженериками, если бы мы сразу добавили тот уровень иерархии, о котором я говорил.
Так вот Вам мой совет, пользуйтесь EJB без интерфейсов, потом когда споткнетесь поймете, почему нужно было вставлять интерфейс. Без ошибок сложно учиться, потому что при реальном опыте все запоминается в контексте.

Sergey Kisel комментирует...

@Nikita Eshkeev, а где аргументация у вашего совета?

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

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

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

Nikita Eshkeev комментирует...

@Sergey Kisel
Не совсем понял вопрос "а где аргументация у вашего совета?", я же написал, что таким образом получается наилучший путь для осознания ошибок и понимания, в чем конкретно была проблема.
А метод проб и ошибок помогает накопить опыт, используя который можно точно предсказать ближашую проблему, при выборе той или иной стратегии развития (так как проблема встречалась ранее).
Таким образом, если EJB без интерфейсов работают для Вас хорошо, то я не буду отговаривать вас использовать этот подход, есть вероятность, что интерфейсы действительно больше не нужны, но возможно для каких-то задач нужно все еще использовать интерейсы, вот когда встретится такая задача, Вы будете точно знать, что тут нужны интерфейсы и у Вас будет точный список аргументов для этого.
По поводу интуиции Вы преувеличиваете, за свою жизнь я поработал на нескольких проектах, и где-то видел одни подходы, где-то другие, иногда просто пользовался тем, что спроектировано и не думал, почему так. Sad but true. Когда на другом проекте приходила задача сделать что-либо, я предлагал подход, который видел на том проекте, но не мог его аргументировать, поэтому и соглашался на другой подход.(почему бы не попробовать новое?) Так если этот подход давал сбой для каких-то задач, я анализировал подход из предыдущего проекта и понимал задумку того архитектора.

Sergey Kisel комментирует...

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

Nikita Eshkeev комментирует...

В самом начале я как раз написал: "Так вот Вам мой совет, пользуйтесь EJB БЕЗ интерфейсов", перечитайте внимательнее.

Sergey Kisel комментирует...

А да, извиняюсь, не так прочитал :)

Владимир Калинин комментирует...

Создавать интерфейсы, на случай вдруг они пригодятся - это лишние телодвижения, которые в 90% случаев ненужны за исключением однотипных операции (например получение элемента по и т.д.). Вместо того, чтобы тратить время на унификацию кода, а при смене ТЗ на вставку в него костылей - проще, дешевле, быстрее написать одноразовое решение.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!