Wiki for course "Java with Jakarta EE".

1. Intro to Jakarta EE

Java EE — это платформа, построенная на основе Java SE, которая предоставляет API и среду времени выполнения для разработки и запуска крупномасштабных, многоуровневых, масштабируемых, надежных и безопасных сетевых приложений.

Подобные приложения называют корпоративными (Enterprise applications), так как они решают проблемы, с которыми сталкиваются большие бизнесы.

Однако использовать подобные приложения и преимущества, которые дает Java EE, могут не только крупные корпорации и правительственные структуры. Решения, которые предлагает платформа Java EE, полезны, а порой просто необходимы отдельным разработчикам и небольшим организациям.

1.1. Развитие Java EE

Java EE развивается по процессу Java Community Process (JCP), сформированному в 1998 году. Он позволяет заинтересованным лицам участвовать в формировании будущих версий спецификаций платформ языка Java.

Основу данного процесса составляют JSR (Java Specification Request — запрос на спецификацию Java), формальные документы, описывающие спецификации и технологии, которые предлагается добавить к Java-платформе. Подобные запросы составляются членами сообщества — простыми разработчиками и компаниями. К числу последних относятся Oracle, Red Hat, IBM, Apache и многие другие.

Т.е. ребята предлагают на рассмотрение новые возможности и плюшки, которые они хотели бы включить в Java. А затем проводят голосование, на основании которого принимается решение, что включить в следующую версию.

История версий Java EE выглядит так:

  • J2EE 1.2 (Декабрь 1999)

  • J2EE 1.3 (Сентябрь 2001)

  • J2EE 1.4 (Ноябрь 2003)

  • Java EE 5 (Май 2006)

  • Java EE 6 (Декабрь 2009)

  • Java EE 7 (Май)

  • Java EE 8 (Август 2017)

  • Jakarta EE 8 (Сентябрь 2019)

В 2017 году произошла новая веха в развитии платформы: Oracle передал контроль над развитием Java EE организации Eclipse Foundation. А в апреле 2018 года Java EE переименовали в Jakarta EE, которая полностью совместима с Java EE 8.

1.2. Архитектура Java EE приложений

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

  • Многоуровневость. Java EE приложения — многоуровневые, и об этом мы еще поговорим подробнее;

  • Вложенность. Есть Java EE сервер (или сервер приложений), внутри него располагаются контейнеры компонентов. В данных контейнерах размещаются компоненты.

Чтобы объяснить архитектуру Java EE приложений, для начала поговорим об уровнях. Какие бывают уровни? Какие Java EE технологии используются на различных уровнях?

Далее мы обсудим, как между собой связаны сервера приложений, контейнеры компонентов и сами компоненты.

Но учтите, что все это — взгляды под различными углами на одно и то же, и очередность тут не так важна.

1.3. Уровни приложений

Многоуровневые приложения — это приложения, которые разделены по функциональному принципу на изолированные модули (уровни, слои).

Обычно, в том числе в контексте Java EE разработки, корпоративные приложения делят на три уровня:

  • клиентский

  • средний уровень

  • уровень доступа к данным

1.3.1. Клиентский уровень.

Представляет собой некоторое приложение, которое запрашивает данные у Java EE сервера (среднего уровня). Сервер, в свою очередь, обрабатывает запрос клиента и возвращает ему ответ. Клиентским приложением может быть браузер, отдельное приложение (мобильное либо настольное) или другие серверные приложения без графического интерфейса.

1.3.2. Средний уровень

Подразделяется, в свою очередь, на web-уровень и уровень бизнес-логики.

Web-уровень

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

На web-уровне используются такие технологии Java EE:

  • JavaServer Faces technology (JSF);

  • Java Server Pages (JSP);

  • Expression Language (EL);

  • Servlets;

  • Contexts and Dependency Injection for Java EE (CDI).

Уровень бизнес-логики.

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

Технологии, которые задействованы на данном уровне:

  • Enterprise JavaBeans (EJB);

  • JAX-RS RESTful web services;

  • Java Persistence API entities;

  • Java Message Service.

1.3.3. Уровень доступа к данным.

Данный уровень иногда называют уровнем корпоративных информационных систем (Enterprise Information Systems, сокращенно —EIS). EIS состоит из различных серверов баз данных, ERP (англ. Enterprise Resource Planning) систем планирования ресурсов предприятия и прочих источников данных. К этому уровню за данными обращается уровень бизнес-логики.

В данном уровне можно встретить такие технологии, как:

  • Java Database Connectivity API (JDBC);

  • Java Persistence API;

  • Java EE Connector Architecture;

  • Java Transaction API (JTA).

1.4. Сервера приложений, контейнеры, компоненты

Взглянем на определение Java EE из Википедии.

Java EE — набор спецификаций и соответствующей документации для языка Java, описывающий архитектуру серверной платформы для задач средних и крупных предприятий.

Чтобы лучше понять, что означает в данном контексте “набор спецификаций”, проведем аналогию с Java-интерфейсом.

Сам по себе Java-интерфейс лишен функциональности. Он просто определяет некоторый контракт, согласно которому реализуется некоторая функциональность.

А вот реализуют интерфейс уже другие классы. Причем у одного интерфейса допустимы несколько реализаций, каждая из которых может друг от друга отличаться некоторыми деталями.

Со спецификацией все точно так же. Голая Java EE — это просто набор спецификаций.

Данные спецификации реализуют различные Java EE сервера.

Java EE сервер — это серверное приложение, которое реализует API-интерфейсы платформы Java EE и предоставляет стандартные службы Java EE. Серверы Java EE иногда называют серверами приложений. Данные сервера могут содержать в себе компоненты приложения, каждый из которых соответствует своему уровню в многоуровневой иерархии. Сервер Java EE предоставляет этим компонентам различные сервисы в форме контейнера.

Контейнеры — это интерфейс между размещенными на них компонентами и низкоуровневыми платформо-независимыми функциональными возможностями, поддерживающими компонент.

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

В Java EE существует четыре различные типа контейнеров:

  1. Контейнеры апплетов выполняются большинством браузеров. При разработке апплетов можно сконцентрироваться на визуальной стороне приложения, в то время как контейнер обеспечивает безопасную среду.

  2. Контейнер клиентского приложения (ACC) включает набор Java-классов, библиотек и других файлов, необходимых для реализации в приложениях Java SE таких возможностей, как внедрение, управление безопасностью и служба именования.

  3. Веб-контейнер предоставляет базовые службы для управления и исполнения веб-компонентов (Servlets, компонентов EJB Lite, страниц JSP, фильтров, слушателей, страниц JSF и веб-служб). Он отвечает за создание экземпляров, инициализацию и вызов Servlets, а также поддержку протоколов HTTP и HTTPS. Этот контейнер используется для подачи веб-страниц к клиент-браузерам.

  4. EJB (Enterprise Java Bean) контейнер отвечает за управление и исполнение компонентов модели EJB, содержащих уровень бизнес-логики приложения. Он создает новые сущности компонентов EJB, управляет их жизненным циклом и обеспечивает реализацию таких сервисов, как транзакция, безопасность, параллельный доступ, распределение, служба именования либо возможность асинхронного вызова.

Также в Java EE выделяют четыре типа компонентов, которые должна поддерживать реализация Java EE спецификации:

  1. Апплеты — это приложения из графического пользовательского интерфейса (GUI), выполняемые в браузере. Они задействуют насыщенный интерфейс Swing API для производства мощных пользовательских интерфейсов.

  2. Приложениями называются программы, выполняемые на клиентской стороне. Как правило, они относятся к графическому пользовательскому интерфейсу (GUI) и применяются для пакетной обработки.

  3. Веб-приложения (состоят из Servlets и их фильтров, слушателей веб-событий, страниц JSP и JSF) — выполняются в веб-контейнере и отвечают на запросы HTTP от веб-клиентов. Servlets также поддерживают конечные точки веб-служб SOAP и RESTful.

  4. Корпоративные приложения (созданные с помощью технологии Enterprise Java Beans, службы сообщений Java Message Service, интерфейса Java API для транзакций, асинхронных вызовов, службы времени) выполняются в контейнере EJB. Управляемые контейнером компоненты EJB служат для обработки транзакционной бизнес-логики. Доступ к ним может быть как локальным, так и удаленным по протоколу RMI (или HTTP для веб-служб SOAP и RESTful).

На диаграмме ниже представлена типичная архитектура Java EE приложения:

Client-Server

1.5. Технологии

Итак, с архитектурой разобрались. Общая структура должна быть ясна. В процессе описания компонентов архитектуры мы затронули некоторые технологии Java EE, такие, как EJB, JSP и пр. Давайте поближе посмотрим на них.

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

Технология Назначение

Servlets

Java-классы, которые динамически обрабатывают клиентские запросы и формируют ответы (обычно HTML страницы).

Java Server Faces (JSF)

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

Java Server Faces Facelets technology

Представляет собой подтип приложения JSF, в котором вместо JSP страниц используются XHTML страницы

Java Server Pages (JSP)

Текстовые документы, которые компилируются в Servlets. Позволяет добавлять динамический контент на статические страницы (например, HTML-страницы)

Java Server Pages Standard Tag Library (JSTL)

Библиотека тегов, в которой инкапсулирована основная функциональность в контексте JSP страниц.

Expression Language

Набор стандартных тегов, которые используются в JSP и Facelets страницах для доступа к Java EE компонентам.

Contexts and Dependency Injection for Java EE (CDI)

Представляет собой набор сервисов, предоставляемых Java EE контейнерами, для управления жизненным циклом компонентов, а также внедрения компонентов в клиентские объекты безопасным способом.

Java Beans Components

Объекты, которые выступают в роли временного хранилища данных для страниц приложения.

В таблице ниже приведены технологии используемые на уровне бизнес-логики:

Технология Назначение

Enterprise Java Beans (enterprise bean) components

EJB — это управляемые компоненты, в которых заключена основная функциональность приложения.

JAX-RS RESTful web services

Представляет собой = API для разработки веб-сервисов, соответствующих архитектурному стилю REST.

JAX-WS web service endpoints

API для создания и использования веб-сервисов SOAP.

Java Persistence API (JPA) entities

API для доступа к данным в хранилищах данных и преобразования этих данных в объекты языка программирования Java и наоборот.

Java EE managed beans

Управляемые компоненты, которые предоставляют бизнес-логику приложения, но не требуют транзакционных функций или функций безопасности EJB.

Java Message Service

API службы сообщений Java (JMS) — это стандарт обмена сообщениями, который позволяет компонентам приложения Java EE создавать, отправлять, получать и читать сообщения. Что обеспечивает распределенную, надежную и асинхронную связь между компонентами.

В таблице ниже приведены технологии, используемые на уровне доступа к данным:

Технология Назначение

Java Database Connectivity API (JDBC)

Низкоуровневое API для доступа и получения данных из хранилищ данных. Типичное использование JDBC — написание SQL запросов к конкретной базе данных.

Java Persistence API (JPA)

API для доступа к данным в хранилищах данных и преобразования этих данных в объекты языка программирования Java и наоборот. Гораздо более высокоуровневое API по сравнению с JDBC. Скрывает всю сложность JDBC от разработчика под капотом.

Java EE Connector Architecture

API для подключения других корпоративных ресурсов, таких как:

  • ERP (англ. Enterprise Resource Planning, система планирования ресурсов предприятия),

  • CRM (англ. Customer Relationship Management, система управления взаимоотношениями с клиентами).

Java Transaction API (JTA)

API для определения и управления транзакциями, включая распределенные транзакции, а также транзакции, затрагивающие множество хранилищ данных.

1.6. Java EE vs Spring

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

В то же время на свет появился Spring IoC. Это была маленькая, красивая и приятная в обращении библиотека. В ней также использовался конфигурационный файл, но в отличие от Java EE, он был один. Простота Spring привела к тому, что практически все стали использовать данный фреймворк в своих проектах.

А далее Spring и Java EE начали свой путь к одному и тому же, но с разных концов. Компания Pivotal Software, разработчик Spring, стали выпускать проект за проектом, чтобы покрыть все возможные и невозможные потребности Java-разработчиков. Постепенно то, что раньше называлось Spring, сначала стало одним из проектов, а потом и вовсе слилось с несколькими другими проектами в Spring Core. Все это привело к неминуемому усложнению Spring по сравнению с тем, каким он был изначально. Со временем следить за всем клубком зависимостей Spring стало уж совсем сложно, и возникла потребность в отдельной библиотеке, которая стала бы загружать и запускать все сама (сейчас где-то икнул так горячо любимый Spring Boot).

Все это время JCP работал над одним — добиться максимального упрощения всего, что только можно внутри Java EE. В итоге в современном EJB для описания Bean достаточно указать одну аннотацию над классом, что предоставляет разработчику доступ ко всей мощи технологии Enterprise Java Beans. И подобные упрощения затронули каждую спецификацию внутри Java EE.

В итоге по функционалу Spring и Java EE примерно разделяют паритет. Где-то что-то лучше, где-то что-то хуже, но если смотреть глобально, больших различий нет. То же самое касается сложности работы. И Spring, и Java EE являются превосходными инструментами. Пожалуй, лучшими из того, что сейчас есть, для построения корпоративных сетевых приложений на языке Java.

Однако, Java EE может работать в общем случае только в рамках Enterprise Application Server’a (Tomcat таковым не является), а приложение на Spring стеке может работать на чем угодно (на том же Tomcat), и даже вообще без сервера (так как запустит его внутри себя самостоятельно).

Это делает Spring идеальным инструментом для разработки небольших приложений с GUI на Front-end или для микросервисной архитектуры. Но отказ от зависимости от серверов приложений отрицательно сказался на масштабируемости Spring-приложений.

А Java EE хорошо подходит для реализации масштабируемого монолитного кластерного приложения.

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

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

2. Введение в Java EE

Java EE или Java Enterprise Edition представляет платформу для создания корпоративных приложений на языке Java. Прежде всего это сфера веб-приложений и веб-сервисов.

Java EE состоит из набора API и среды выполнения. Некоторые из API:

  • Java Servlets. Сервлеты представляют специальные модули, которые обрабатывают запросы от пользователей и отправляют результат обработки.

  • JavaServer Pages (JSP). Также модули на стороне сервера, которые обрабатывают запросы. Удобны для генерации большого контента HTML. По сути предствляют собой страницы с кодом HTML/JavaScript/CSS с вкраплениями кода на Java

  • Enterprise JavaBeans (EJB) представляют классы, которые хранят бизнес-логику.

  • Contexts and Dependency Injection (CDI) предоставляет механизм для внедрения и управления зависимостями в другие объекты.

  • JSON Processing (JSON-P) позволяет работать со строками JSON в Java

  • JSON Binding (JSON-B) предоставляет функционал для сериализации и десериализации JSON в объекты Java.

  • WebSocket позволяет интегрировать WebSocket в приложения на Java.

  • Java Message Service (JMS) - API для пересылки сообщений между двумя и более клиентами.

  • Security API - API для стандартизации и упрощения задач обеспечения безопасности в приложениях на Java.

  • Java API for RESTful Web Services (JAX-RS) - API для применения архитектуры REST в приложениях.

  • JavaServer Faces (JSF) предоставляет возможности для создания пользовательского интерфейса на стороне сервера.

Эти и ряд других API сообственно и образуют то, что называется Java EE. Стоит отметить, что также в среде веб-разработки на Java популярна еще одна технология Spring. Фреймворк Spring не является частью Java EE и может использоваться как альтернативный подход к созданию веб-приложений на языке Java.

2.1. История развития

Предтечей Java EE был проект JPE Project, который стартовал в мае 1998 года. А в декабре 1999 года вышел релиз платформы Enterprise Java Platform (J2EE 1.2), которая объединяла такие компоненты как Servlet, JSP, EJB, JMS. В 2006 году с выходом 5-й версии она была переименована в Java Enterprise Edition (JEE). С тех пор периодически выходят новые версии платформы. Последняя текущая версия - Java EE 8 вышла в сентябре 2017 года.

В 2017 году произошла новая веха в развитии платформы: Oracle передал контроль над развитием Java EE организации Eclipse Foundation. А в апреле 2018 года Java EE была переименована в Jakarta EE.

В начале 2019 года ожидается выход новой версии Jakarta/Java EE.

2.2. Установка IDE

Для работы с Java EE нам потребуется среда разработки или IDE. Есть различные среды разработки, которые ориентированы на корпоративную разрабоку под Java. Это IntelliJ IDEA, NetBeans и Eclipse. В данном случае на протяжении всего руководства мы преимущественно будем использовать Eclipse, потому что она является бесплатной и довольно широко распространена.

Для начала установим последнюю версию Eclipse, которую можно найти по адресу Eclipse. На странице загрузок выберем найдем рядом с названием текущей версии Eclipse кнопку Download и нажмем на нее.

Eclipse download

После нажатия на кнопку нас перенаправит собственно на страницу загрузки, где необходимо будет нажать на кнопку "Download" для загрзуки установочного пакета:

Eclipse download

После ее загрузки программы установки запустим ее и в отобразившемся списке опций выберем Eclipse IDE for Java EE Developers:

Eclipse tools Jakarta EE download

Далее надо будет указать папку для установки (или оставить выбранную папку по умолчанию) и принять пару лицензионных соглашений, и среда будет установлена.

3. Apache Tomcat

3.1. Установка Tomcat

Tomcat представляет веб-контейнер сервлетов и предназначен для работы с рядом технологий Java EE, в частности, с JSP, Servlet и рядом других. Нередко Tomcat называют веб-сервером.

В данном случае используется 9-я версия. Для установки Tomcat перейдем на официальный сайт данного контейнера на страницу загрузок - Apache Tomcat. На данной странице мы можем увидеть различные опции для загрузки:

  • в виде архива, который достаточно распаковать

  • в виде инсталлятора.

Binary Distributions

В данном случае большой разницы не будет, какой именно пакет использовать. Для упрощения настройки выберем пункт 32-bit/64-bit Windows Service Installer.

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

Installation window

Вначале надо принять лицензионное соглашение:

Installation window

Затем надо настроить устанавливаемые компоненты:

Installation window

Здесь можно выбрать те компоненты, которые мы хотим установить, в частности, можно выбрать пункт Service Startup, и тогда Tomcat будет запускаться автоматически при запуске системы. Можно выбрать все, а можно ограничиться теми компонентами, которые уже выбраны по умолчанию.

Далее будет предложено настроить порты и ряд дополнительных моментов конфигурации Tomcat:

Installation window

Здесь стоит обратить внимание на пункт HTTP/1.1 Connector Port. Он указывает, по какому порту будет запускаться приложение. Укажем в этом поле номер 8081.

Затем надо будет указать версию Java, которая будет использоваться:

Installation window

По умолчанию инсталлятор должен определять путь к Java. Но естественно при необходимости его можно изменить. И в конце надо будет указать путь к устанавливаемому веб-контейнеру на жестком диске:

Installation window

Также можно оставить путь по умолчанию. И после этого собственно произойдет установка контейнера.

Installation window

После установки на финальном экране оставим отмеченнным пункт Run Apache Tomcat и нажмем на кнопку Finish. После этого Tomcat будет запущен, и мы сможем к нему обращаться.

Убедимся, что Tomcat работает. Для этого обратимся в строке браузера по адресу http://localhost:8081. В данном случае 8081 - это тот порт, который был указан на этапе установке выше. Если все Tomcat установлен и запущен правильно, то в браузере мы увидим некоторое стандартное содержимое:

Apache Tomcat welcom page

3.2. Структура Apache Tomcat

После установки в директории Apache Tomcat на жестком диске можно найти ряд файлов и директорий.

Tomcat files structure

Основные каталоги:

  • bin хранит различные скрипты, в частности, для запуска, перезагрузки и т.д.

  • conf хранит конфигурационные файлы, наиболее важным из которых является файл server.xml, который определяет основную часть конфигурации.

  • logs директория по умолчанию для хранения лог-файлов.

  • webapps директория, где хранятся собственно файлы приложений.

Таким образом, все файлы приложений будут помещаться в директорию webapps. По умолчанию после установки в ней можно найти ряд стандартных директорий:

Tomcat Webapps

Ключевая директория здесь - это директория ROOT. Так, все содержимое, которое мы видим в браузере по адресу http://localhost:8081, как раз представляет файлы из этой директории.

Для примера создадим в директории webapps новую директорию test.

Далее создадим где-нибудь на жестком диске новый файл hello.html со следующим содержимым:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Tomcat</title>
</head>
<body>
    <h2>Hello Apache Tomcat!!!</h2>
</body>
</html>

Этот обычный файл с кодом html. Затем скопируем этот файл в выше созданную директорию test.

Tomcat Webapps test

После этого мы сможем обратиться к этому файлу в браузере по адресу http://localhost:8081/test/hello.html. То есть в начале указывается локальный адрес localhost, затем номер порта (в данном случае 8081), далее название директории - test, и далее название файла - hello.html.

Tomcat Webapps page

Стотит отметить, что при обращении к файлам в директории ROOT, нам не надо указывать название директории. Например, скопируем тот же файл hello.html в папку ROOT. Тогда к этому файлу мы можем обратиться по адресу http://localhost:8081/hello.html

Tomcat hello page

Несмотря на то, что по умолчанию директория ROOT уже содержит некоторое содержимое справочного характера, тем не менее мы можем все это содержимое спокойно удалить и размещать в этой директории только файлы непосредственно нашего приложения. То есть мы вполне можем использовать директорию ROOT. Но как правило, приходится работать с несколькими приложениями, для которых лучше создавать отдельную директорию в директории webapps.

4. Jakarta Servlet

4.1. Введение в сервлеты

Сервлет представляет специальный тип классов Java, который выполняется на веб-сервере и который обрабатывает запросы и возвращает результат обработки.

Создадим первый сервлет. Определим где-нибудь на жестком диске файл HelloServlet.java со следующим кодом:

import java.io.PrintWriter;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		response.setContentType("text/html");
		PrintWriter writer = response.getWriter();
		try {
			writer.println("<h2>Hello from HelloServlet</h2>");
		} finally {
			writer.close();
		}
	}
}

Класс сервлета наследуется от класса HttpServlet. Перед определением класса указана аннотация @WebServlet, которая указывает, с какой конечной точкой будет сопоставляться данный сервлет. То есть данный сервлет будет обрабатывать запросы по адресу /hello.

Для обработки GET-запросов (например, при обращении к сервлету из адресной строки браузера) сервлет должен переопределить метод doGet(). То есть, к примеру, в данном случае get-запрос по адресу /hello будет обрабатываться методом doGet().

Этот метод принимает два параметра. Параметр типа HttpServletRequest инкапсулирует всю информацию о запросе. А параметр типа HttpServletResponse позволяет управлять ответом. В частности, с помощью вызова response.setContentType("text/html") устанавливается тип ответа (в данном случае, мы говорим, что ответ представляет код html). А с помощью метода getWriter() объекта HttpServletResponse мы можем получить объект PrintWriter, через который можно отправить какой-то определенный ответ пользователю. В данном случае через метод println() пользователю отправляет простейший html-код. По завершению использования объекта HttpServletResponse его необходимо закрыть с помощью метода close().

Для запуска сервлета воспользуемся опять же контейнером сервлетов Apache Tomcat. В каталоге Tomcat в папке webapps создадим каталог для нового приложения, который назовем helloapp.

В папке приложения классы сервлетов должны размещаться в папке WEB-INF/classes. Создадим в каталоге helloapp папку WEB-INF, а в ней папку classes. И в папку helloapp/WEB-INF/classes поместим файл HelloServlet.java.

servlet1

Но нам нужен не код сервлета, а скомпилированный класс сервлета. Поэтому скомпилируем сервлет. Для этого нам нужно использовать специальную утилиту servlet-api.jar, которая находится в каталоге Tomcat в папке lib.

В начале в командной строке/терминале перейдем с помощью команды cd к папке helloapp/WEB-INF/classes, где расположен код сервлета.

Потом для компиляции сервлета выполним следующую команду:

javac -cp .;"C:\Program Files\Apache Software Foundation\Tomcat 9.0\lib\servlet-api.jar" HelloServlet.java

В моем случае предполагается, что Tomcat размещен в каталоге C:\Program Files\Apache Software Foundation\Tomcat 9.0.

servlet2

После этого в папке helloapp/WEB-INF/classes должен появиться класс сервлета. Перезапустим Tomcat и обратимся к нашему сервлету в браузере:

servlet3

Поскольку с помощью аннотации @WebServlet в классе сервлета была указана точка /hello, то при обращении к сервлету после домена и порта идет название приложения (helloapp) и конечная точка (hello).

4.2. Как работает сервлет

Сервлет - это класс, который расширяет функциональность класса HttpServlet и запускается внутри контейнера сервлетов.

Сервлет размещается на сервере, однако чтобы сервер мог использовать сервлет для обработки запросов, сервер должен поддерживать движок или контейнер сервлетов (servlet container/engine). Например, Apache Tomcat по сути является контейнером сервлетов, поэтому он может использовать сервлеты для обслуживания запросов.

Для обработки запроса в HttpServlet определен ряд методов, которые мы можем переопределить в классе сервлета:

  • doGet() обрабатывает запросы GET (получение данных)

  • doPost() обрабатывает запросы POST (отправка данных)

  • doPut() обрабатывает запросы PUT (отправка данных для изменения)

  • doDelete() обрабатывает запросы DELETE (удаление данных)

  • doHead() обрабатывает запросы HEAD

Каждый метод обрабатывает определенный тип запросов HTTP, и мы можем определить все эти методы, но, зачастую, работа идет в основном с методами doGet() и doPost(). Например, определение методов без реализации:

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    }

    protected void doPut(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    }

    protected void doDelete(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    }

    protected void doHead(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    }
}

Все методы в качестве параметра принимают два объекта:

  • HttpServletRequest хранит информацию о запросе

  • HttpServletResponse управляет ответом на запрос.

4.2.1. Жизненный цикл сервлета

Для каждого сервлета движок сервлетов создает только одну копию. Вне зависимости от того, сколько запросов будет отправлено сервлету, все запросы будут обрабатываться толькой одной копией сервлета. Объект сервлета создается либо при запуске движка сервлетов, либо когда сервлет получает первый запрос. Затем для каждого запроса запускается поток, который обращается к объекту сервлета.

При работе с сервлетом движок сервлетов вызывает у класса сервлета ряд методов, которые определены в родительском абстрактном классе HttpServlet.

Когда движок сервлетов создает объект сервлета, у сервлета вызывается метод init().

public void init(ServletConfig config) throws ServletException {
}

Этот метод вызывается только один раз - при создании сервлета. Мы можем переопределить этот метод, чтобы определить в нем некоторую логику инициализации.

Когда к сервлету приходит запрос, движок сервлетов вызывает метод service() сервлета. А этот метод, исходя из типа запроса (GET, POST, PUT и т.д.) решает, какому методу сервлета (doGet(), doPost() и т.д.) обрабатывать этот запрос.

public void service(HttpServletRequest request, HttpServletResponse response)
		throws IOException, ServletException {
}

Этот метод также можно переопределить, однако в этом нет смысла. В реальности для обработки запроса переопределяются методы onGet(), onPost() и т.д., которые обрабатывают конкретные типы запросов.

Если объект сервлета долгое время не используется (к нему нет никаких запросов), или если происходит завершение работы движка сервлетов, то движок сервлетов выгружает из памяти все созданные экземпляры сервлетов. Однако до выгрузки сервлета из памяти у сервлета вызывается метод destroy().

public void destroy() {
}

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

Lifecycle

Поскольку для обработки всех запросов создается один экземпляр сервлета, и все обращения к нему идут в отдельных потоках, то не рекомендуется в классе сервлета объявлять и использовать глобальные переменные, так как они не будут потокобезопасными.

4.3. Получение данных в сервлете

В методы doGet() и doPost() сервлета, которые обрабатывают запрос, в качестве одного из параметров передается объект HttpServletRequest, с помощью которого можно получить отправленные сервлету данные, то есть параметры запроса. Для этого в классе HttpServletRequest определены два метода:

  • getParameter(String param) возвращает значение определенного параметра, название которого передается в метод. Если указанного параметра в запросе нет, то возвращается значение null.

  • getParameterValues(String param) возвращает массив значений, который представляет определенный параметр. Если указанного параметра в запросе нет, то возвращается значение null.

4.3.1. Получение данных из строки запроса

Передавать значения в сервлет можно различными способами. При отправке GET-запроса значения передаются через строку запроса. Стандартный get-запрос принимает примерно следующую форму: название_ресурса?параметр1=значение1&параметр2=значение2.

Например, определим следующий сервлет:

import java.io.PrintWriter;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        // получаем параметр id
        String id = request.getParameter("id");
        try {
            writer.println("<h2>Id:" + id + "</h2>");
        } finally {
            writer.close();
        }
    }
}

В данном случае мы предполагаем, что в сервлет в get-запросе передается значение/параметр по имени id. С помощью вызова request.getParameter("id") мы получаем значение этого параметра и затем отправляем его в ответ пользователю. При этом стоит учитывать, что неважно, что мы будем передавать извне - целое или дробное число, отдельный символ, в любом случае метод getParameter() возвратить строку.

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

serlvet7

В случае выше значение для параметра id не указано, мы просто вводим в адресной строке браузера название ресурса - в данном случае http://localhost:8081/helloapp/hello, где helloapp - название приложения, а hello - конечная точка, к которой примонтирован сервлет. Поэтому метод getParameter("id") возвращает значение null.

Теперь передадим значение для параметра id:

servlet8

Параметры в строке запроса указываютя после названия ресурса после знака вопроса:` http://localhost:8081/helloapp/hello?id=5`.В данном случае параметр id равен 5.

Подобным образом можно передавать несколько параметров. Например, изменим сервлет следующим образом:

import java.io.PrintWriter;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        String name = request.getParameter("name");
        String age = request.getParameter("age");

        try {
            writer.println("<h2>Name: " + name + "; Age: " + age + "</h2>");
        } finally {
            writer.close();
        }
    }
}

В данном случае сервлет получает два параметра: name и age. Мы можем передать значения для них, набрав в адресной строке, например,` http://localhost:8081/helloapp/hello?name=Tom&age=34.` При передаче некольких параметров они отделяются друг от друга знаком амперсанда &. Подобным образом мы можем передать и большее количество параметров.

servlet9

4.3.2. Передача массивов

В примерах выше передавались одиночные данные - отдельные строки. Но также мы можем передавать наборы значений. Например, изменим сервлет следующим образом:

import java.io.PrintWriter;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        String[] nums = request.getParameterValues("nums");

        try {
            writer.print("<h2>Numbers: ");
            for(String n: nums)
                writer.print(n + " ");
            writer.println("</h2>");
        } finally {
            writer.close();
        }
    }
}

Если в сервлет передается массив значений, то для его получения у объекта HttpServletRequest применяется метод getParameterValues(), который получает название параметра и возвращает массив строк. В данном случае мы ожидаем, что параметр будет называться nums. Затем значения из этого параметра в цикле передаются в ответ клиенту.

Запустим сервлет и передадим ему значения

servlet10

При передаче массива через строку запроса указываются несколько значений с одним и тем же именем: http://localhost:8081/helloapp/hello?nums=1&nums=2&nums=3.

4.3.3. Получение данных из форм

Еще одним распространенным способом отправки данных является отправка форм. Добавим в проект страницу index.html со следующим кодом:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>User Form</title>
</head>
<body>
    <form action="hello" method="POST">
        Name: <input name="username" />
        <br><br>
        Age: <input name="userage" />
        <br><br>
        Gender: <input type="radio" name="gender" value="female" checked />Female
        <input type="radio" name="gender" value="male" />Male
        <br><br>
        Country: <select name="country">
            <option>Canada</option>
            <option>Spain</option>
            <option>France</option>
            <option>Germany</option>
        </select>
        <br><br>
        Courses:
        <input type="checkbox" name="courses" value="JavaSE" checked />Java SE
        <input type="checkbox" name="courses" value="JavaFX" checked />Java FX
        <input type="checkbox" name="courses" value="JavaEE" checked />Java EE
        <br><br>
        <input type="submit" value="Submit" />
    </form>
</body>
</html>

Как правило, формы отправлются с помощью запроса POST, поэтому у элемента формы определен атрибут method="POST". Сама форма будет отправляться на ресурс /hello, с которым будет сопоставляться сервлет. На самой форме есть множество полей ввода, в том числе набор чекбоксов, из которых можно выбрать сразу несколько значений.

servlet11

Теперь определим сервлет, который будет обрабатывать эту форму:

import java.io.PrintWriter;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();

        String name = request.getParameter("username");
        String age = request.getParameter("userage");
        String gender = request.getParameter("gender");
        String country = request.getParameter("country");
        String[] courses = request.getParameterValues("courses");

        try {
            writer.println("<p>Name: " + name + "</p>");
            writer.println("<p>Age: " + age + "</p>");
            writer.println("<p>Gender: " + gender + "</p>");
            writer.println("<p>Country: " + country + "</p>");
            writer.println("<h4>Courses</h4>");
            for (String course: courses) {
                writer.println("<li>" + course + "</li>");
            }
        } finally {
            writer.close();
        }
    }
}

Данный сервлет будет обрабатывать запросы к по адресу /hello, на который отпавляется форма. Поскольку отправка формы осущетвляется с помощью метода POST, то для обработки запроса определен метод doPost(). Метод doPost() принимает те же параметры, что и метод doGet().

С помощью методов getParameter() и getParameterValues() также получаем значения параметров. В данном случае названия параметров представляют названия полей ввода отправленной формы.

Причем поскольку на форме несколько чекбоксов, которые имеют одно и то же имя, то соответственно сервлету передается набор значений, который получаем с помощью метода getParameterValues(). Все остальные поля представляют одиночные значения, поэтому для получения их данных применяется метод getParameter().

servlet12

4.4. Переадресация и перенаправление запроса

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

4.4.1. Перенаправление запроса

Метод forward() класса RequestDispatcher позволяет перенаправить запрос из сервлета на другой сервлет, html-страницу или страницу jsp. Причем в данном случае речь идет о перенаправлении запроса, а не о переадресации.

Например, пусть в проекте определена страница index.html :

<!DOCTYPE html>
<html>
    <head>
    <meta charset="UTF-8">
    <title>Servlets in Java</title>
</head>
<body>
    <h2>Index.html</h2>
</body>
</html>

Данная страница просто выводит заголовок.

И, допустим, мы хотим из сервлета перенаправить запрос на эту страницу:

import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String path = "/index.html";
        ServletContext servletContext = getServletContext();
        RequestDispatcher requestDispatcher = servletContext.getRequestDispatcher(path);
        requestDispatcher.forward(request, response);
    }
}

Для того, чтобы выполнить перенаправление запроса, вначале с помощью метода getServletContext() получаем объект ServletContext , который представляет контекст запроса. Затем с помощью его метода getRequestDispatcher() получаем объект RequestDispatcher . Путь к ресурсу, на который надо выполнить перенаправление, передается в качестве параметра в getRequestDispatcher() .

Затем у объекта RequestDispatcher вызывается метод forward(), в который передаются объекты HttpServletRequest и HttpServletResponse .

И если мы обратимся к сервлету, то фактически мы получим содержимое страницы index.html , который будет перенаправлен запрос.

JSP-hello

Подобным образом мы можем выполнять перенаправление на страницы jsp и другие сервлеты. Например, добавим в проект новый сервлет NotFoundServlet :

NotFoundServlet

Определим для NotFoundServlet следующий код:

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/notfound")
public class NotFoundServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        String id = request.getParameter("id");
        try {
            writer.println("<h2>Not Found: " + id + "</h2>");
        } finally {
            writer.close();
        }
    }
}

В данном случае NotFoundServlet сопоставляется с адресом /notfound.

Изменим код HelloServlet , чтобы он перенаправлял на NotFoundServlet:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

        String path = "/notfound";
            ServletContext servletContext = getServletContext();
            RequestDispatcher requestDispatcher = servletContext.getRequestDispatcher(path);
            requestDispatcher.forward(request, response);
    }
}

В данном случае если id равен null , то идет перенаправление на NotFoundServlet. Следует отметить, что в метод requestDispatcher.forward передаются объекты HttpServletRequest и HttpServletResponse. То есть NotFoundServlet получит те же самые данные запроса, что и HelloServlet.

Not Found JSP-pages

4.4.2. Переадресация

Для переадресации применяется метод sendRedirect() объекта HttpServletResponse. В качестве параметра данный метод принимает адрес переадресации. Адрес может быть локальным, внутренним, а может быть и внешним.

Например, если сервлету HelloServlet не передано значение для параметра id, выполним переадресацию на сервлет NotFoundServlet:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
        String id = request.getParameter("id");
        if (id == null) {
            String path = request.getContextPath() + "/notfound";
            response.sendRedirect(path);
        } else {
            response.setContentType("text/html");
            PrintWriter writer = response.getWriter();
            try {
                writer.println("<h2>Hello Id " + id + "</h2>");
            } finally {
                writer.close();
            }
        }
    }
}

В данном случае переадресация идет на локальный ресурс. Но важно понимать, что в метод sendRedirect передается адрес относительно корня текущего домена. То есть в данном случае у нас домен и порт http://localhost:8001/, а приложение называется helloapp, то для обращения к сервлету NotFoundServlet необходимо передать адрес helloapp/notfound. Путь к текущему приложению можно получить с помощью метода getContextPath().

Также можно выполнять и переадресацию на внешний ресурс, указывая полный адрес:

response.sendRedirect("https://metanit.com/");

4.5. web.xml и маппинг сервлетов

При определении сервлетов применялась аннотация @WebServlet, которая устанавливала конечную точку, с которой сопоставлялся сервлет. Например:

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
}

Вышеприведенный сервлет сопоставляется с путем "/hello". То есть сервлет HelloServlet будет обрабатывать запросы типа "название_приложения/hello" или "название_приложения/hello".

Но есть и другой способ сопоставления путей и сервлетов. Он представляет использование файла web.xml.

Файл web.xml хранит информацию о конфигурации приложения. Он не является обязательной частью приложения, тем не менее он широко используется для настройки конфигурации.

Данный файл должен располагаться в папке WEB-INF приложения. При запуске Tomcat считывает его содержимое и использует считанную конфигурацию. Если же файл содержит ошибки, то Tomcat отображает ошибку.

web.xml имеет определенную структуру. Все вложенные узлы, которые определяют конфигурацию, помещаются в корневой узел <web-app>.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                        http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
    version="4.0">
</web-app>

У элемента web-app определяется ряд атрибутов. В данном случае атрибуты xmlns и xmlns:xsi указывают на используемые пространства имен xml. Атрибут version указывает на версию спецификации сервлетов или Servlet API, которая используется в приложении. Последняя версия API сервлетов - 4.0.

С помощью элемента <servlet-mapping> можно задать сопоставление сервлета с запрашиваемым URL.

Например, добавим в проект в Eclipse в директорию WebConent/WEB-INF новый файл web.xml:

web.xml

Определим в нем следующий код:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                        http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
    version="4.0">

    <servlet>
        <servlet-name>HelloWorld</servlet-name>
        <servlet-class>HelloServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>HelloWorld</servlet-name>
        <url-pattern>/welcome</url-pattern>
    </servlet-mapping>

</web-app>

Прежде всего вначале с помощью элемента <servlet> определяется сервлет. Элемент <servlet-name> задает имя сервлета, на которое будет проецировать класс, указанный в элементе <servlet-class>. То есть, допустим, в проекте есть класс сервлета HelloServlet, который будет проецироваться на имя HelloWorld. Имя может быть произвольным и может совпадать с названием класса.

Затем в элементе <servlet-mapping> сервлет с именем HelloWorld (по сути сервлет HelloServlet) сопоставляется с путем /welcome.

Допустим, сервлет HelloServlet будет выглядеть следующим образом:

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        try {
            writer.println("<h2>Welcome to servlets</h2>");
        } finally {
            writer.close();
        }
    }
}

Теперь, чтобы обратиться к этому сервлету, надо использовать путь /welcome:

Request welcom

4.6. Параметры инициализации сервлетов

4.6.1. Общие параметры

Элемент <context-param> в файле web.xml задает параметр, которым инициализируются сервлеты. Внутри этого элемента с помощью элемента <param-name> задается имя параметра, а с помощью элемента <param-value> - значение параметра.

Например, определим следующий файл web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                        http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
    version="4.0">

    <context-param>
        <param-name>message</param-name>
        <param-value>Hello Servlets</param-value>
    </context-param>

</web-app>

В данном случае параметр называется message и имеет значение Hello Servlets. Этот параметр доступен всем сервлетам в приложении.

Для получения параметра внутри сервлета необходимо обратиться к контексту сервлета. Для его получения внутри класса сервлета определен метод getServletContext(). Он возвращает объект ServletContext. Затем у этого объекта вызывается метод getInitParameter(), в который передается название параметра инициализации.

Например, получим выше определенный параметр в сервлете:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String message = getServletContext().getInitParameter("message");
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        try {
            writer.println("<h2>" + message + "</h2>");
        } finally {
            writer.close();
        }
    }
}

Hello page

Стоит отметить, что этот параметр будет доступен для всех сервлетов в приложении. Соответственно если мы изменим в web.xml значение параметра, то оно изменится для всех сервлетов.

4.6.2. Параметры для отдельного сервлета

Кроме определения общих для всех сервлетов параметров, мы можем определить параметры непосредственно для конкретного сервлета. Для этого внутри элемента <servlet> определяется элемент <init-param>

Например, изменим web.xml следующим образом:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                        http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
    version="4.0">
    <servlet>
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>HelloServlet</servlet-class>
        <init-param>
            <param-name>message</param-name>
            <param-value>Hello Servlets</param-value>
        </init-param>
    </servlet>
</web-app>

Здесь также задан параметр message со значением Hello Servlets. Чтобы получить этот параметр в классе сервлете, необходимо использовать метод getServletConfig(), который возвращает объект ServletConfig. У него затем вызывается метод getInitParameter(), в который передается имя параметра. Например:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String message = getServletConfig().getInitParameter("message");
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        try {
            writer.println("<h2>" + message + "</h2>");
        } finally {
            writer.close();
        }
    }
}

4.7. Обработка ошибок

Файл web.xml позволяет указать, какие страницы html или jsp будут отправляться пользователю при отправке статусных кодов ошибок. Для этого в web.xml применяется элемент <error-page>.

Внутри этого элемента с помощью элемента <error-code> указывается статусный код ошибки, который надо обработать. А элемент <location> указывает на путь к странице html или jsp, которая будет отправляться пользователю.

Например, добавим в проект в папку WebContent новый файл 404.html со следующим кодом:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Not Found</title>
</head>
<body>
    <h2>Resource not found!</h2>
</body>
</html>

Page 404

В файле web.xml определим следующее содержимое:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                        http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
    version="4.0">
    <error-page>
        <error-code>404</error-code>
        <location>/404.html</location>
    </error-page>
</web-app>

В данном случае элемент error-code указывает, что мы будем обрабатывать ошибки со статусным кодом 404 (то есть такие ошибки, которые подразумевают отсутствие ресурса на сервере). А элемент location указывает, что в случае обращения к несуществующему ресурсу пользователю будет отправляться страница 404.html.

Page Not Found

4.7.1. Обработка исключений

Кроме настройки обработки стандартных ошибок протокола http, типа 404 или 403, файл web.xml позволяет настроить обработку исключений, которые могут возникнуть при обработке запроса. Для этого в web.xml применяется элемент <exception-type>.

Например, добавим в проект в папку WebContent новый файл error.jsp и определим в нем следующий код:

<%
   String message = pageContext.getException().getMessage();
   String exception = pageContext.getException().getClass().toString();
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
    <title>Exception</title>
</head>
<body>
    <h2>Exception occurred while processing the request</h2>
    <p>Type: <%= exception%></p>
    <p>Message: <%= message %></p>
</body>
</html>

Данная страница jsp будет отображать информацию об исключении. Через глобальный объект pageContext в страницу передается контекст. Если при обработке запроса возникло какое-нибудь исключение, то метод pageContext.getException() возвратит это исключение в виде объекта Exception. И далее мы можем исследовать этот объект и вызывать его методы, например, получить тип исключения и сообщение об исключении.

Симитируем с сервлете какое-нибудь исключение:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        int x = 0;
        int y = 8 / x;
    }
}

В данном случае мы получаем ошибку деления на нуль, которая представлена типом java.lang.ArithmeticException.

Теперь определим следующий файл web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                        http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
    version="4.0">
    <error-page>
        <exception-type>java.lang.Throwable</exception-type>
        <location>/error.jsp</location>
    </error-page>
</web-app>

Элемент exception-type указывает, что обрабатываться будут исключения типа java.lang.Throwable. Поскольку это базовый класс для всех типов исключений, то фактически мы будем обрабатывать все исключения. Хотя можно конкретизировать тип исключения, например, указать тот же java.lang.ArithmeticException.

Элемент location определяет страницу, которая отправляется пользователю при возникновении исключении. В данном случае это error.jsp.

В итоге при обращении к сервлету будет сгенерировано исключение, и мы увидим информацию о нем:

Page exception

4.8. Cookies

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

Куки могут быть двух типов:

  • сеансовые куки, т.е. когда пользователь закрывает вкладку браузера и прекращает работать с приложением, то куки сеанса уничтожаются.

  • постоянные куки - хранятся в течение продолжительного времени (до 3 лет).

Следует учитывать некоторые ограничения. Прежде всего куки нередко ограничены по размеру (обычно не более 4 килобайт). Кроме того, обычно браузеры принимают не более 20 кук с одного сайта. Более того, в некоторых браузерах может быть отключена поддержка кук.

Для работы с куками сервлеты могут используют класс javax.servlet.http.Cookie. Для создания куки надо создать объект этого класса с помощью констуктора Cookie(String name, String value), где name - ключ, а value - значение, которое сохраняется в куках. Стоит отметить, что мы можем сохранить в куках только строки.

Чтобы добавить куки в ответ клиенту у объекта HttpServletResponse применяется метод addCookie(Cookie c).

При создании куки мы можем использовать ряд методов объекта Cookie для установки и получения отдельных параметров:

  • setMaxAge(int maxAgeInSeconds) устанавливает время в секундах, в течение которого будут существовать куки. Специальное значение -1 указывает, что куки будут существовать только в течение сессии и после закрытия браузера будут удалены.

  • setValue(String value) устанавливает хранимое значение.

  • getMaxAge() возвращает время хранения кук.

  • getName() возвращает ключ кук.

  • getValue() возвращает значение кук.

Например, установка куки с названием user и значением Tom:

Cookie cookie = new Cookie("user", "Tom");
response.addCookie(cookie);

Чтобы получить куки, которые приходят в запросе от клиента, применяется метод getCookies() класса HttpServletRequest.

Например, получение куки по имени:

Cookie[] cookies = request.getCookies();
String cookieName = "user";
Cookie cookie = null;
if(cookies != null) {
    for(Cookie c: cookies) {
        if(cookieName.equals(c.getName())) {
            cookie = c;
            break;
        }
    }
}

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

Например, определим сервлет SetServlet, который будет устанавливать куки:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/set")
public class SetServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        try {
            response.addCookie(new Cookie("user", "Tom"));
            out.println("Cookie is set");
        }
        finally {
            out.close();
        }
    }
}

В данном случае устанавливается куки user, которая хранит строку Tom.

Определим сервлет HelloServlet, который получает эту куку:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        Cookie[] cookies = request.getCookies();
        String cookieName = "user";
        Cookie cookie = null;
        if(cookies != null) {
            for(Cookie c: cookies) {
                if(cookieName.equals(c.getName())) {
                    cookie = c;
                    break;
                }
            }
        }
        PrintWriter out = response.getWriter();
        try {
            out.println("Name: " + cookie.getValue());
        }
        finally {
            out.close();
        }
    }
}

Таким образом, при обращении к сервлету SetServlet произойдет установка кук, а при обращении к сервлету HelloServlet мы получим установленные куки:

Cookies

4.9. Sessions

Сессия позоляет сохранять некоторую информацию на время сеанса. Когда клиент обращается к сервлету или странице JSP, то движок сервлетов проверяет, определен ли в запросе параметр ID сессии. Если такой параметр неопределен (например, клиент первый раз обращается к приложению), тогда движок сервлетов создает уникальное значение ID и связанный с ним объект сессии. Объект сессии сохраняется на сервере, а ID отправляется в ответе клиенту и по умолчанию сохраняется на клиенте в куках. Затем когда приходит новый запрос от того же клиента, то движок сервлетов опять же может получить ID и сопоставить его с объектом сессии на веб-сервере.

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

Для получения объекта сессии в сервлете у объекта HttpServletRequest определен метод getSession(). Он возвращает объект HttpSession.

HttpSession session = request.getSession();

Для управления сессией объект HttpSession предоставляет ряд методов:

  • setAttribute(String name, Object o) сохраняет в сессии под ключом name

  • getAttribute(String name) возвращает из сессии объект с ключом name. Если ключа name в сессии неопределено, то возвращается null

  • removeAttribute(String name) удаляет из сессии объект с ключом name

Например, определим следующий сервлет:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // получаем сессию
        HttpSession session = request.getSession();
        // получаем объект name
        String name = (String) session.getAttribute("name");

        PrintWriter out = response.getWriter();
        try {
            // если объект ранее не установлен
            if (name == null) {
                // устанавливаем объект с ключом name
                session.setAttribute("name", "Tom Soyer");
                out.println("Session data are set");
            } else {
                out.println("Name: " + name);
                // удаляем объект с ключом name
                session.removeAttribute("name");
            }
        } finally {
            out.close();
        }
    }
}

В данном случае мы получаем из сессии объект с ключом name. Если он не определен, то добавляем его в сессию, если определен - то удаляем. Таким образом, при первом запросе к приложению, в сессию будут добавлены данные, а при втором удалены, но мы сможем увидеть эти данные:

Session

Подобным образом можно сохранять в сессию более сложные объекты. Допустим, у нас есть следующий класс, который представляет пользователя:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

Сохраним в сессию и получим из сессии объект этого класса:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session = request.getSession();
        Person tom = (Person) session.getAttribute("person");

        PrintWriter out = response.getWriter();
        try {
            if (tom == null) {
                tom = new Person("Tom", 34);
                session.setAttribute("person", tom);
                out.println("Session data are set");
            } else {
                out.println("Name: " + tom.getName() + " Age: " + tom.getAge());
                session.removeAttribute("person");
            }
        } finally {
            out.close();
        }
    }
}

В данном случае также если объект по ключу person не установлен, то он устанавливается, иначе удаляется.

4.9.1. Дополнительные методы HttpSession

Кроме выше рассмотренных методов HttpSession есть еще ряд методов, которые могут быть полезны. Некоторые из них:

  • getAttributeNames() возвращает объект java.util.Enumeration, который содержит все ключи имеющих в сессии объектов

  • getId() возвращает идентификатор сессии в виде строки

  • isNew() возвращает true, если для клиента еще не установлена сессия (клиент сделал первый запрос или на клиенте отключены куки)

  • setMaxInactiveInterval(int seconds) устанавливает интервал неактивности в секундах. И если в течение этого интервала клиент был неактивен, то данные сессии данные удаляются. По умолчанию максимальный интервал неактивности 1800 секунд. Значение -1 указывает, что сессия удаляется только тогда, когда пользователь закрыл вкладку в браузере.

  • invalidate() удаляет из сессии все объекты

Применение методов:

// получение всех ключей
Enumeration keys = session.getAttributeNames();
while (keys.hasMoreElements()) {
    System.out.println((String) keys.nextElement());
}

session.setMaxInactiveInterval(60 * 60 * 24); // установка интервала неактивности 1 день
session.setMaxInactiveInterval(-1); // до закрытия браузера

// удаление всех данных из сессии
session.invalidate();

4.10. Filters

Filter, в соответствии со спецификацией, это Java-код, пригодный для повторного использования и позволяющий преобразовать содержание HTTP-запросов, HTTP-ответов и информацию, содержащуюся в заголовках HTML.

Filter занимается предварительной обработкой запроса, прежде чем тот попадает в сервлет, и/или последующей обработкой ответа, исходящего из сервлета.

filter

Filters могут:

  • перехватывать инициацию сервлета прежде, чем сервлет будет инициирован;

  • определить содержание запроса прежде, чем сервлет будет инициирован;

  • модифицировать заголовки и данные запроса, в которые упаковывается поступающий запрос;

  • модифицировать заголовки и данные ответа, в которые упаковывается получаемый ответ;

  • перехватывать инициацию сервлета после обращения к сервлету.

4.10.1. Интерфейс javax.servlet.Filter

Filter может быть сконфигурирован так, что он будет работать с одним сервлетом или группой сервлетов. Основой для формирования фильтров служит интерфейс javax.servlet.Filter, который реализует три метода:

  • void init (FilterConfig config) throws ServletException

  • void destroy()

  • void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException

Метод init() вызывается прежде, чем фильтр начинает работать, и определяет конфигурационные параметры фильтра. Метод doFilter() выполняет непосредственно работу фильтра. Таким образом, сервер вызывает init() один раз, чтобы запустить фильтр в работу, а затем вызывает doFilter() столько раз, сколько запросов будет сделано непосредственно к данному фильтру.

После того как фильтр заканчивает свою работу, вызывается метод destroy().

package common;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class FilterConnect implements Filter {
    private FilterConfig config = null;
    private boolean active = false;

    public void init (FilterConfig config) throws ServletException {
        this.config = config;
        String act = config.getInitParameter("active");
        if (act != null) {
            active = (act.toUpperCase().equals("TRUE"));
        }
    }

    public void doFilter (ServletRequest request, ServletResponse response,
                          FilterChain chain) throws IOException, ServletException {
        if (active) {
            // Здесь можно вставить код для обработки
        }
        chain.doFilter(request, response);
    }

    public void destroy() {
        config = null;
    }
}
  • В примере представленного шаблона фильтра инициализируется значение параметра active. При вызове фильтра (метод doFilter()) проверяется значение параметра active, и если active равно true, могут быть выполнены действия, определенные разработчиком.

  • Интерфейс FilterConfig содержит метод для получения имени фильтра, его параметров инициации и контекста активного в данный момент сервлета.

  • С помощью своего метода doFilter() каждый фильтр получает текущий запрос ServletRequest и ответ ServletResponse, а также FilterChain, содержащий список фильтров, предназначенных для обработки.

  • В методе doFilter() фильтр может делать с запросом и ответом всё, что ему захочется - собирать данные или упаковывать объекты для придания им нового поведения.

  • Затем фильтр вызывает chain.doFilter(), чтобы передать управление следующему фильтру.

  • После возвращения этого вызова, фильтр может по окончании работы своего метода doFilter() выполнить дополнительную работу над полученным ответом. К примеру, сохранить регистрационную информацию об этом ответе.

После того как класс-фильтр откомпилирован, его необходимо установить в контейнер сервлетов и привязать (mapping) к одному или нескольким сервлетам. Объявление и подключение фильтра определяется в дескрипторе приложения web.xml внутри элементов <filter> и <filter-mapping>. Для подключения фильтра к сервлету необходимо использовать вложенные элементы <filter-name> и <servlet-name>.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="4.0" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
   http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd">
    ...
    <filter>
        <filter-name>FilterName</filter-name>
        <filter-class>common.FilterConnect</filter-class>
        <init-param>
            <param-name>active</param-name>
            <param-value>true</param-true>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>FilterName</filter-name>
        <servlet-name>ServletName</servlet-name>
    </filter-mapping>
    ...
</web-app>

В представленном коде дескриптора приложения web.xml объявлен класс-фильтр FilterConnect с именем FilterName. Фильтр имеет параметр инициализации active, которому присваивается значение true. Фильтр FilterName в разделе <filter-mapping> подключен к сервлету ServletName.

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

  • цепочка, определяемая url-pattern, выстраивается в том порядке, в котором встречаются соответствующие описания фильтров в web.xml;

  • последовательность сервлетов, определенных с помощью servlet-name, также выполняется в той последовательности, в какой эти элементы встречаются в дескрипторе поставки web.xml.

filter chain

Для связи фильтра со страницами HTML или группой сервлетов необходимо использовать тег <url-pattern>. Например, после следующего кода:

<filter-mapping>
    <filter-name>FilterName</filter-name>
    <url-pattern>*.html</url-pattern>
</filter-mapping>

Такой фильтр будет применен ко всем вызовам страниц HTML.

Привязывать фильтры можно также с использованием аннотации @WebFilter():

@WebFilter(name = "FilterName, url", urlPatterns={}, servletNames={})
public class FilterConnect implements Filter {
    // ...
}

В данном случае порядок вызова фильтров ничем не гарантирован.

4.10.2. Использование дополнительных ресурсов, RequestDispatcher

В отдельных случаях недостаточно вставить в сервлет фильтр или даже цепочку фильтров, а необходимо обратиться к другому сервлету, странице JSP, документу HTML, XML или другому ресурсу.

filter forward

Если требуемый ресурс находится в том же контексте, что и сервлет, который его вызывает, то для получения ресурса необходимо использовать метод представленный в интерфейсе ServletRequest:

  • getRequestDispatcher(String path): RequestDispatcher, где path - это путь к ресурсу относительно контекста.

Например, необходимо обратиться к сервлету Connect:

RequestDispatcher rd = request.getRequestDispatcher("Connect");

Если ресурс находится в другом контексте, то необходимо предварительно получить контекст методом интерфейса ServletContext:

  • getContext(String uripath): ServletContext;

После чего получить RequestDispatcher с помощью использовать метода интерфейса ServletContext:

  • getRequestDispatcher(String uripath): RequestDispatcher, где путь uripath должен быть абсолютным, т.е. начинаться с наклонной черты /.

Например:

RequestDispatcher rd = config.getServletContext()
                                .getContext("/prod")
                                .getRequestDispatcher("/prod/Customer");

Если требуемый ресурс - сервлет, помещенный в контекст под своим именем, то для его получения можно обратиться к методу интерфейса ServletContext:

  • getNamedDispatcher (String name): RequestDispatcher

Все эти методы возвращают null, если ресурс недоступен или сервер не реализует интерфейс RequestDispatcher.

Как видно из описания методов, к другим ресурсам можно обратиться только через объект типа RequestDispatcher, который предлагает два метода обращения к ресурсу:

  • forward(ServletRequest request, ServletResponse response): void

  • include(ServletRequest request, ServletResponse response): void

forward(ServletRequest request, ServletResponse response): void просто передает управление другому ресурсу, предоставив ему свои аргументы ServletRequest и ServletResponse. Вызывающий сервлет выполняет предварительную обработку объектов request и response и передает их вызванному сервлету или другому ресурсу, который окончательно формирует ответ response и отправляет его клиенту или, опять-таки, вызывает другой ресурс. Например:

if (rd != null) {
    rd.forward (request, response);
} else {
    response.sendError (HttpServletResponse.SC_NO_CONTENT);
}

Вызывающий сервлет не должен выполнять какую-либо отправку клиенту до обращения к методу forward(), иначе будет выброшено исключение класса IllegalStateException. Если же вызывающий сервлет уже что-то отправлял клиенту, то следует обратиться ко второму методу

include(ServletRequest request, ServletResponse response): void - этот метод вызывает ресурс, который на основании объекта request может изменить тело объекта response. Но вызванный ресурс не может изменить заголовки и код ответа объекта response. Это естественное ограничение, поскольку вызывающий сервлет мог уже отправить заголовки клиенту. Попытка вызванного ресурса изменить заголовок будет просто проигнорирована. Можно сказать, что метод include() выполняет такую же работу, как вставки на стороне сервера SSI(Server Side Include).

4.11. Listeners

Событие (Event) - это то, что произошло. В мире веб-приложений событием может быть инициализация приложения, уничтожение приложения, запрос от клиента, создание/уничтожение сеанса, изменение атрибутов в сеансе и т.д.

Servlet API предоставляет различные типы интерфейсов Listeners (слушателей), которые мы можем реализовать и настроить в web.xml для обработки определенных событий, когда происходят определенные события. Например, можно создать Listener для события запуска приложения, чтобы прочитать параметры инициализации контекста и создать соединение с базой данных, а также установить его как свойство контекста для использования другими ресурсами.

4.11.1. Интерфейс Listener и объект Event

Servlet API предоставляет разные типы Listeners для разных типов событий. Интерфейс Listener объявляет методы для обработки подобного набора событий. Например, у нас есть ServletContextListener, который прослушивает события запуска и завершения контекста. Каждый метод в интерфейсе Listener принимает объект Event в качестве входных данных. Объект Event действует как оболочка, предоставляя конкретный объект для Listener.

Servlet API предоставляет следующие объекты Event:

  • javax.servlet.AsyncEvent- событие, которое вызывается, когда асинхронная операция, инициированная ServletRequest (путем вызова ServletRequest.startAsync() или ServletRequest.startAsync(ServletRequest, ServletResponse)), завершилась по тайм-ауту или вызвала ошибку.

  • javax.servlet.http.HttpSessionBindingEvent - событие этого типа отправляются объекту, который реализует HttpSessionBindingListener, когда объект связан или не связан с сеансом, или HttpSessionAttributeListener, настроенному в web.xml, когда любой атрибут привязывается, отвязывается или заменяется в session (сеансе). Сеанс связывает объект, вызывая HttpSession.setAttribute(), и отменяет привязку объекта, вызывая HttpSession.removeAttribute(). Когда объект удаляется из сеанса, мы можем использовать это событие для операций очистки.

  • javax.servlet.http.HttpSessionEvent - это класс, который представляет уведомления о событиях изменения сеанса в веб-приложении.

  • javax.servlet.ServletContextAttributeEvent - класс событий для уведомлений об изменениях атрибутов ServletContext веб-приложения.

  • javax.servlet.ServletContextEvent - это класс событий для уведомлений об изменениях контекста сервлета веб-приложения.

  • javax.servlet.ServletRequestEvent - этот тип события представляет событие жизненного цикла ServletRequest. Исходный код события - ServletContext этого веб-приложения.

  • javax.servlet.ServletRequestAttributeEvent - это класс событий для уведомления об изменениях атрибутов, запрошенных сервлетом в приложении.

Servlet API предоставляет следующие интерфейсы Listener:

  • javax.servlet.AsyncListener - слушатель будет уведомлен, если асинхронная операция, запущенная в ServletRequest с добавленным слушателем, завершилась, истекло время ожидания или вызвала ошибку.

  • javax.servlet.ServletContextListener - интерфейс для получения уведомлений об изменениях жизненного цикла ServletContext.

  • javax.servlet.ServletContextAttributeListener - интерфейс, который получает события уведомления об изменениях атрибута ServletContext.

  • javax.servlet.ServletRequestListener - интерфейс для получения событий уведомления о запросах, входящих и выходящих из области веб-приложения.

  • javax.servlet.ServletRequestAttributeListener - интерфейс, который получает события уведомления об изменениях атрибута ServletRequest.

  • javax.servlet.http.HttpSessionListener - интерфейс для получения уведомлений об изменениях жизненного цикла HttpSession.

  • javax.servlet.http.HttpSessionBindingListener - приводит к уведомлению объекта, когда он связан с или из сеанса.

  • javax.servlet.http.HttpSessionAttributeListener - интерфейс для получения событий уведомления об изменениях атрибута HttpSession.

  • javax.servlet.http.HttpSessionActivationListener - объекты привязанные к сеансу, могут прослушивать события контейнера, сообщая им, что сеанс будет деактивирован или активирован. Контейнер, который переносит сеанс между виртуальными машинами или сохраняет сеансы, необходим для уведомления всех объектов, привязанных к сеансам, реализующим HttpSessionActivationListener.

4.11.2. Настройка Listener

Можно использовать аннотацию @WebListener для объявления класса как Listener, но класс должен реализовывать один или несколько интерфейсов Listener.

Можно определить слушателей в web.xml:

<listener>
    <listener-class>
        com.example.listener.AppContextListener
    </listener-class>
</listener>

5. Jakarta Server Pages

5.1. What is JSP

Java Server Pages представляет технологию, которая позволяет создавать динамические веб-страницы. Изначально JSP (вместе с сервлетами) на заре развития Java EE являлись доминирующим подходом к веб-разработке на языке Java. И хотя в настоящее время они уступили свое место другой технологии - JSF, тем не менее JSP продолжают широко использоваться.

По сути Java Server Page или JSP представляет собой html-код с вкраплениями кода Java. В то же время станицы jsp - это не стандартные html-страницы. Когда приходит запрос к определенной странице JSP, то сервер обрабатывает ее, генерирует из нее код html и отправляет его клиенту. В итоге пользователь после обращения к странице JSP видит в своем браузере обычную html-страницу.

Как и обычные статические веб-страницы, файлы JSP необходимо размещать на веб-сервере, к которому обычные пользователи могут обращаться по протоколу http, например, набирая в адресной строке браузера нужный адрес. Однако чтобы сервер мог обрабатывать файлы JSP, он должен использовать движок JSP (JSP engine), который также называют JSP-контейнером. Есть множество движков JSP, и все они реализуют одну и ту же спецификацию и в целом работают одинаково. Однако тем не менее при переносе кода с одного веб-сервера на другой могут потребоваться небольшие изменения.

Shema connection

В данном случае для работы с JSP мы будем использовать Apache Tomcat, который одновременно является и веб-сервером и контейнером сервлетов и JSP. Создадим простейшую страницу JSP. Для этого где-нибудь на жестком диске определим файл index.jsp. Все станицы JSP имеют расширение jsp. Откроем этот файл в любом текстовом редакторе и определим в нем следующий код:

<%
String header = "Apache Tomcat";
%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>First JSP App</title>
</head>
<body>
    <h2><%= header %></h2>
    <p>Today <%= new java.util.Date() %></p>
</body>
</html>

С помощью тегов <% …​ %> мы можем определить код Java на странице JSP. В данном случае мы просто определяем переменную типа String, которая называется header.

Затем идет стандартный код страницы html. Чтобы внедрить код java внутрь html-страницы применяются теги <%= %> - после знака равно указывается выражение Java, результат которого будет выводиться вместо этих тегов. В данном случае, используются две таких вставки. Первая вставка - значение переменной header, которая была определена выше. Вторая вставка - выражение new java.util.Date(), которое возвращает текущую дату.

Для тех, кто знаком с веб-разработкой на PHP, это может напоминать оформление файлов php, которые также содержать код html и код php.

Теперь поместим данный файл на сервер - в данном случае в контейнер Tomcat. Перейдем в Apache Tomcat к папке webapps\ROOT. Удалим из нее все содержимое и поместим нашу страницу index.jsp, которая была создана выше.

Index jsp

Запустим Apache Tomcat (если он не запущен), и обратимся к приложению по адресу http://localhost:xxxx/index.jsp, где xxxx - номер порта, по которому запущен Tomcat:

Page jsp

В итоге Tomcat получит запрос к странице index.jsp, обработает код на java, сгенерирует html-страницу и отправит ее пользователю.

По умолчанию Apache Tomcat настроен таким образом, что все запросы к корню приложения по умолчанию обрабатываются страницей index.jsp, поэтому мы также можем обращаться к ней по адресу http://localhost:xxxx.

5.2. JSP syntax basics

Содержимое страницы JSP фактически делится на код html (а также css/javascript) и код на языке Java. Для вставки кода java на страницу JSP можно использовать пять основных элементов:

  • Выражения JSP (JSP Expression)

  • Скриплет JSP (JSP Scriplet)

  • Объявления JSP (JSP Declaration)

  • Директивы JSP (JSP Directive)

  • Комментарии JSP

5.2.1. JSP Expression

JSP Expression представляет выражение, заключенное между тегами <%= и %>. При обращении к JSP вычисляется значение этого выражения.

Например, определим следующую страницу JSP:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>First JSP App</title>
</head>
<body>
    <p>2 + 2 = <%= 2 + 2 %></p>
    <p>5 > 2 = <%= 5 > 2 %></p>
    <p><%= new String("Hello").toUpperCase() %></p>
    <p>Today <%= new java.util.Date() %></p>
</body>
</html>

Здесь используются четыре JSP-выражения. Первое выражение - простая арифметическая операция сложения. При обработке страницы вместо

<p>2 + 2 = <%= 2 + 2 %></p>

будет сгенерирована следующая html-разметка:

<p>2 + 2 = 4</p>

Второе выражение - операция сравнения во многом аналогична. Третье выражение - создание объекта String и вызов у него метода toUpperCase(), который возвращает строку в верхнем регистре. То есть выражение также может представлять вызов метода.

И четвертое выражение - вызов конструктора класса Date, который создает объект с текущей датой.

Когда придет запрос к этой странице, из нее будет сгенерирован следующий код:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>First JSP App</title>
    </head>
    <body>
        <p>2 + 2 = 4</p>
        <p>5 > 2 = true</p>
        <p>HELLO</p>
        <p>Today Fri Aug 31 11:37:26 MSK 2018</p>
    </body>
</html>

И соответственно эту страницу мы увидим в браузере:

JSP syntax basics

5.2.2. JSP Scriplet

JSP Scriplet представляет одну или несколько строк на языке Java. Скриплет заключается внутри следующих тегов:

<%
    код Java
%>

Например, определим следующую страницу JSP:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>First JSP App</title>
</head>
<body>
    <%
        for (int i = 1; i < 5; i++) {
            out.println("<br>Hello " + i);
        }
    %>
</body>
</html>

В данном случае скриплет представляет цикл for, в котором генерируется вывод с помощью метода out.println(). В итоге в браузере будет выведено четыре разо слово Hello с соответствующей цифрой:

JSP syntax basics

Другой пример - определим переменную и массив и выведим их содержимое на страницу:

<%
    String[] people = new String[]{"Tom", "Bob", "Sam"};
    String header = "Users list";
%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>First JSP App</title>
</head>
<body>
    <h3><%= header %></h3>
    <ul>
    <%
        for (String person: people) {
            out.println("<li>" + person + "</li>");
        }
    %>
    </ul>
</body>
</html>

В скриплете в начале страницы определяются две переменных - переменная типа String и массив строк. С помощью второго скриплета содержимое массива через цикл for выводится на страницу.

JSP syntax basics

5.2.3. JSP Declaration

JSP Declaration позволяют определить метод, который мы затем можем вызывать в скриплетах или в JSP-выражениях. Определение метода помещается между тегами <%! и %>. Например, определим следующую JSP-страницу:

<%!
    int square(int n){
        return n * n;
    }
%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>First JSP App</title>
</head>
<body>
    <p><%= square(6) %></p>
    <ul>
    <%
        for(int i = 1; i <= 5; i++){
            out.println("<li>" + square(i) + "</li>");
        }
    %>
    </ul>
</body>
</html>

В данном случае метод square() возвращает квадрат числа. Затем этот метод используется в выражении JSP и в скриплете в цикле for.

JSP syntax basics

5.2.4. Директивы

Директивы предназначены для установки условий, которые применяются ко всей странице JSP. Наиболее используемая директива - это директива page. Например, с помощью атрибута import этой директивы мы можем импортировать пакеты или отдельные классы на страницу JSP.

Например, в первом коде статьи для вывода даты использовалось выражение new java.util.Date(). Но мы можем использовать данное выражение на странице многократно, либо использовать другие классы из пакета java.util. И в этом случае мы можем импортировать нужные нам классы или пакеты:

<%@ page import="java.util.Data" %>

Импорт всего пакета:

<%@ page import="java.util.*" %>

Если необходимо импортировать несколько классов и(или) пакетов, то они перечисляются через запятую:

<%@ page import="java.util.Data, java.text.*" %>

Другой полезный и часто используемый атрибут - pageEncoding, который задает кодировку UTF-8. Например:

<%@ page import="java.util.Date" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>JSP Application</title>
</head>
<body>
    <h2>Сегодня: <%= new Date() %></h2>
</body>
</html>

JSP syntax basics

5.2.5. Комментарии

Комментарии JSP добавляются с помощью тега <%-- Текст_комментария --%>:

<%-- Первое приложение на JSP --%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>First JSP App</title>
</head>
<body>
    <h2>Hello</h2>
</body>
</html>

При этом внутри скриплета мы можем использовать стандартные для языка Java комментарии:

<%
    /*
        Пример цикла
        в JSP
    */
    // вывод строки Hello четыре раза
    for(int i = 1; i < 5; i++){
        out.println("<br>Hello " + i);
}
%>

5.3. Using Java classes in JSP

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

В данном случае используем Eclipse. Создадим новый проект по типу Dynamic Web Project (как описано в прошлой теме) или возьмем уже имеющийся.

Допустим, в моем случае проект называется hellojsp. Перейдем в структуре проекта к узлу Java Resources и его под узлу src. Это тот узел, который хранит в проекте все классы на языке Java, которые мы хотим использовать в своем приложении.

Вначале добавим в папку src новый пакет, где будут располагаться классы. Поэтому нажмем на узел src правой кнопкой мыши и в контекстном меню выберем пункт New → Package.

Creat package

В появившемся окне укажем название пакета, например, com.metanit.hellojsp:

Creat package

Далее добавим в этот пакет новый класс. Для этого в структуре проекта нажмем на пакет правой кнопкой мыши и в контекстном меню выберем пункт New → Class:

Creat servlet

В окне добавления нового класса дадим ему имя Calculator (имя может быть произвольным). Остальные опции оставим по умолчанию.

Create JavaBean

Далее определим в файле Calculator.java следующий код:

package com.metanit.hellojsp;

public class Calculator {
    public int square (int n) {
        return n * n;
    }
}

В данном случае для простоты примера класс содержит один метод, который возводит число в квадрат.

Теперь определим в проекте в папке WebContent файл jsp, например, hello.jsp. Используем на странице hello.jsp класс Calculator. Для этого определим в hello.jsp следующий код:

<%@page import="com.metanit.hellojsp.Calculator"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>JSP Application</title>
</head>
<body>
    <h2>Square of 3 = <%= new Calculator().square(3) %></h2>
</body>
</html>

Вначале с помощью директивы @page подключается класс com.metanit.hellojsp.Calculator. Затем его метод square() используется на странице.

Hello JSP

5.4. Вложение jsp-страниц

В одну JSP-страницу можно вставлять несколько других. Это позволяет определять некоторые общие блоки для всех страниц и использовать их повторно на нескольких страницах JSP.

Для этого применяется специальный тег jsp:include, который может использоваться как обычный html-элемент на страницах JSP.

Определим следующий проект.

Project

Допустим, у нас есть файл header.html с простейшим кодом:

<nav><a href="#">Home</a> | <a href="#">Contact</a> | <a href="#">About</a></nav>
<h2>Hello JSP</h2>

В данном случае здесь определен обычный код html.

Также пусть в проекте будет определен файл footer.jsp со следующим содержимым:

<p>Copyright ©Simon & Schuster, Inc. 2002.</p>

В данном случае мы определили два файла, которые представляют соответственно условный хедер и условный футер - части стандартной веб-страницы.

И также определим в проекте файл index.jsp со следующим кодом:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>JSP Application</title>
    </head>
    <body>
        <jsp:include page="header.html" />
        <p>Main Content 1</p>
        <p>Main Content 2</p>
        <p>Main Content 3</p>
        <jsp:include page="footer.jsp" />
    </body>
</html>

С помощью тега jsp:include содержимое обоих файлов вставляется в данную jsp-страницу. Атрибут page указывает на адрес вставляемого файла. Причем это может быть и обычная html-страница, и jsp-файл.

При обращении к странице index.jsp мы сможем увидеть на странице содержимое вставляемых файлов:

Result

5.5. Получение отправленных форм

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

  • getParameter(String param) возвращает значение определенного параметра, название которого передается в метод. Если указанного параметра в запросе нет, то возвращается значение null.

  • getParameterValues(String param) возвращает массив значений, который представляет определенный параметр. Если указанного параметра в запросе нет, то возвращается значение null.

5.5.1. Получение данных из строки запроса

Например, определим в проекте файл postuser.jsp, который будет получать отправленные в запросе данные:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>User Info</title>
    </head>
    <body>
        <p>Name: <%= request.getParameter("name") %></p>
        <p>Age: <%= request.getParameter("age") %></p>
    </body>
</html>

В данном случае мы получаем два параметра: name и age.

Запустим приложение и передадим странице данные через строку запроса postuser.jsp?name=Bob&age=29:

Request with params

Если для определенного параметра не передается значение, то страница получает значене null:

Request with params

5.5.2. Получение данных из форм

Подобным образом можно получать данные из отправленных форм. Например, определим в проекте файл index.html с формой ввода:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>User Form</title>
</head>
<body>
    <form action="postuser.jsp">
        Name: <input name="name" />
        <br>
        Age: <input name="age" type="number" min=1 />
        <br>
        <input type="submit" value="Submit" />
    </form>
</body>
</html>

В данном случае форма предназначена для ввода имени и возаста пользователя. И по нажатию на кнопку данные формы уходят странице postuser.jsp.

JSP postuser

Таким образом, после ввода данных и их отправке на странице index.html эти данные будут получены скриптом postuser.jsp:

JSP with form

Рассмотрим отправку более сложной формы. Определим в файле index.html следующий код:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>User Form</title>
</head>
<body>
    <form action="postuser.jsp" method="POST">
        Name: <input name="username" />
        <br>
        Gender: <input type="radio" name="gender" value="female" checked />Female
        <input type="radio" name="gender" value="male" />Male
        <br>
        Country: <select name="country">
            <option>Iran</option>
            <option>Turkey</option>
            <option>China</option>
            <option>Germany</option>
        </select>
        <br>
        Courses:
        <input type="checkbox" name="courses" value="JavaSE" checked />Java SE
        <input type="checkbox" name="courses" value="JavaFX" checked />Java FX
        <input type="checkbox" name="courses" value="JavaEE" checked />Java EE
        <br>
        <input type="submit" value="Submit" />
    </form>
</body>
</html>

Здесь имеется сразу несколько полей ввода - обычное текстовое поле, выпадающий список select, набор радиокнопок и набор элементов checkbox, где пользователь одновременно может выбрать сразу несколько полей.

Для обработки этой формы определим в postuser.jsp следующий код:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>User Info</title>
</head>
<body>
    <p>Name: <%= request.getParameter("username") %></p>
    <p>Country: <%= request.getParameter("country") %></p>
    <p>Gender: <%= request.getParameter("gender") %></p>
    <h4>Selected courses</h4>
    <ul>
    <%
        String[] courses = request.getParameterValues("courses");
        for(String course: courses){
            out.println("<li>" + course + "</li>");
        }
    %>
    </ul>
</body>
</html>

Для тех полей, которые допускают выбор одного значения или представляют одно значение, все идентично: после слова param через точку указывается название поля. Но, например, набор чекбоксов имеет одно и то же имя - courses и позволяет выбрать сразу несколько значений. Эти значения мы можем получить в JSP через массив:

String[] courses = request.getParameterValues("courses");

И затем можно использовать цикл for для перебора массива и вывода на страницу его элементов.

Page with form

5.6. Передача данных из сервлета в jsp

Нередко страница jsp обрабатывает запрос вместе сервлетом. В этом случае сервлет определяет логику, а jsp - визуальную часть. И при обработке запроса сервлет может перенаправить дальнейшую обработку странице jsp. Соответственно может возникнуть вопрос, как передать данные из сервлета в jsp?

Есть несколько способов передачи данных из сервлета в jsp, которые заключаются в использовании определенного контекста или scope. Есть несколько контекстов для передачи данных:

  • request (контекст запроса) данные сохраняются в HttpServletRequest

  • session (контекст сессии) данные сохраняются в HttpSession

  • application (контекст приложения) данные сохраняются в ServletContext

Данные из контекста запроса доступны только в пределах текущего запроса. Данные из контекста сессии доступны только в пределах текущего сеанса. А данные из контекста приложения доступны постоянно, пока работает приложение.

Но вне зависимости от выбранного способа передача данных осуществляется с помощью метода setAttribute(name, value), где name - строковое название данных, а value - сами данные, которые могут представлять различные данные.

Наиболее распространенный способ передачи данных из сервлета в jsp представляют атрибуты запроса. То есть у объекта HttpServletRequest, который передается в сервлет, вызывается метод setAttribute(). Этот метод устанавливает атрибут, который можно получить в basic.jsp и вывести его значение на страницу.

5.6.1. Request scope

Определим сервлет HelloServlet, который передает запрос basic.jsp и передает ей данные:

HelloServlet.java
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.setAttribute("name", "Tom");
        request.setAttribute("age", 34);

        getServletContext().getRequestDispatcher("/basic.jsp").forward(request, response);
    }
}

Сервлет устанавливает два атрибута - name и age через объект HttpServletRequest и затем перенаправляет запрос странице basic.jsp.

basic.jsp
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
<title>JSP Application</title>
</head>
<body>
    <p>Name: <%= request.getAttribute("name")%></p>
    <p>Age: <%= request.getAttribute("age")%></p>
</body>
</html

Если мы обратимся к servlet HelloServlet, то он передаст запрос и данные странице basic.jsp.

JSP hello

5.6.2. Session scope

Подобным образом можно передать данные в jsp через сессию:

basic.jsp
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
<title>JSP Application</title>
</head>
<body>
    <p>Name: <%= request.getSession().getAttribute("name")%></p>
    <p>Age: <%= request.getSession().getAttribute("age")%></p>
</body>
</html>
HelloServlet.java
import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        HttpSession session = request.getSession();
        session.setAttribute("name", "Tom");
        session.setAttribute("age", 34);

        getServletContext().getRequestDispatcher("/basic.jsp").forward(request, response);
    }
}

5.6.3. Application scope

Использование контекста приложения представляет применение объекта ServletContext, который можно получить в servlet с помощью метода getServletContext():

HelloServlet.java
import java.io.IOException;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        ServletContext servletContext = getServletContext();
        selvletContext.setAttribute("name", "Tom");
        selvletContext.setAttribute("age", 35);

        getServletContext().getRequestDispatcher("/basic.jsp").forward(request, response);
    }
}
basic.jsp
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
<title>JSP Application</title>
</head>
<body>
    <p>Name: <%= request.getServletContext().getAttribute("name")%></p>
    <p>Age: <%= request.getServletContext().getAttribute("age")%></p>
</body>
</html>

6. Jakarta Standard Tag Library

6.1. JSTL

JSP по умолчанию позволяет встраивать код на java в разметку html. Однако иногда использование стандартных способов для ряда операций, например, для ывод на страницу элементов из списка и т.д., может быть несколько громоздким. Чтобы упростить встраивание кода java в JSP была разработана специальная библиотека - JSTL. JSTL (JSP Standard Tag Library) предоставляет теги для базовых задач JSP (цикл, условные выражения и т.д.)

Эта библиотека не является частью инфраструктуры Java EE, поэтому ее необходимо добавлять в проект самостоятельно. Библиотеку можно найти по адресу maven artifact. Из репозитория нам необходимо загрузить файл jstl-1.2.jar.

download jstl in java ee

В проекте Eclipse эту библиотеку необходимо добавить в папку WebContent/WEB-INF/lib:

JSTL в Java EE

Несмотря на то, что JSTL часто называется библиотекой, на самом деле она содержит ряд библиотек:

  • Core: содержит основные теги для наиболее распространенных задач. Использует префикс c и uri http://java.sun.com/jsp/jstl/core

  • Formatting: предоставляет теги для форматирования чисел, дат, времени. Использует префикс fmt и uri http://java.sun.com/jsp/jstl/fmt

  • SQL: предоставляет теги для sql-запросами и источниками данных. Использует префикс sql и uri http://java.sun.com/jsp/jstl/sql

  • XML: предоставляет теги для работы с xml. Использует префикс x и uri http://java.sun.com/jsp/jstl/xml

  • Functions: предоставляет функции для работы со строками. Использует префикс fn и uri http://java.sun.com/jsp/jstl/functions

Для подключения функционала этих библиотек на страницу jsp применяется директива taglib. Например, подключения основной библиотеки:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

Рассмотрим некоторые базовые и наиболее используемые возможности JSTL.

6.1.1. Защита от внедрения кода

Стандартный синтаксис EL не позволяет нам экранировать теги. Например, допустим, в сервлете в jsp передается некоторый текст, который содержит код html:

public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        request.setAttribute("message", "<script>alert('Hello gold')</script>");
        getServletContext().getRequestDispatcher("/index.jsp").forward(request, response);
    }
}

Передаваемая здесь строка содержит javascript-код, который отображает диалоговое окно. Но естественно код мог бы быть менее безобидным. И если в jsp для вывода атрибута message применяется только стандартный EL:

<p>${message}</p>

то в результате этот html-код будет внедрен напрямую в страницу:

XSS in JSP in Java EE

Теперь используем возможности JSTL:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>JSTL</title>
</head>
<body>
    <p><c:out value="${message}" /></p>
</body>
</html>

Тег c:out позволяет декодировать теги html. Его атрибут value указывает на выводимое значение. В итоге мы получим совсем другой вывод:

Защита от XSS в JSP в Java EE

6.1.2. Значение по умолчанию

Через атрибуты сервлет передает данные в JSP. Однако вполне возможно, что из-за каких-то условий нужное значение не будет передано. В этом случае мы можем установить для тега <c:out> значение по умолчанию, используя его параметр default:

<p><c:out value="${message2}" default="Hello world"/></p>

И если значение для атрибута message2 не передано, то для него будет использоваться строка "Hello world".

6.2. Основные возможности JSTL

Рассмотрим некоторые основные возможности JSTL.

6.2.1. Цикл

Вывод в цикле элементов массива или коллекции:

<c:forEach var="user" items="${users}">
    <p>${user}</p>
</c:forEach>

В данном случае параметр items указывает на коллекцию, элементы которой выводятся. А параметр var задает переменную, через которую доступен текущий перебираемый элемент.

Например, в сервлет передается массив строк:

public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        String[] users = new String[]{"Tom", "Bob", "Sam"};
        request.setAttribute("users", users);
        getServletContext().getRequestDispatcher("/index.jsp").forward(request, response);
    }
}

В jsp мы можем получить и вывести элементы массива следующим образом:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>User Info</title>
</head>
<body>
    <ul>
        <c:forEach var="user" items="${users}">
            <li><c:out value="${user}" /></li>
        </c:forEach>
    </ul>
</body>
</html>

Цикл в JSTL в JSP

Еще один пример - вывод в цикле всех куки:

<ul>
<c:forEach var="cook" items="${cookie}">
    <li>
        <p><c:out value="${cook.value.name}" /></p>
        <p><c:out value="${cook.value.value}" /></p>
    </li>
</c:forEach>
</ul>

6.2.2. Условные выражения

Выражение if:

<c:if test="${isVisible == true}">
    <p>Visible</p>
</c:if>

В данном случае если атрибут isVisible равен true, то выводится код, который расположен между тегами <c:if> и </c:if>

Если надо задать альтернативную логику, то можно добавить тег c:if, который проверяет противоположное условие:

<c:if test="${isVisible == true}">
    <p>Visible</p>
</c:if>
<c:if test="${isVisible == false}">
    <p>Invisible</p>
</c:if>

6.2.3. Тег choose

Тег c:choose подобно конструкции switch…​case в Java проверяет объект на соответствие одному из значений. Например

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>JSTL in JSP</title>
</head>
<body>
    <c:choose>
    <c:when test="${val == 1}">
        <p>Equals to 1</p>
    </c:when>
    <c:when test="${val == 2}">
        <p>Equals to 2</p>
    </c:when>
    <c:otherwise>
        <p>Undefined</p>
    </c:otherwise>
    </c:choose>
</body>
</html>

В данном случае тег c:choose проверяет значение атрибута val. Для проверки применяются вложенные теги c:when, которые аналогичны блокам case в конструкции switch..case. С помощью их параметра test значение атрибута сравнивается с определенным значением. И если выражения сравнения истинно, то выводится код, который размещен внутри данного элемента c:when. Таким образом мы можем определить несколько блоков c:when. Дополнительный тег <c:otherwise> выполняется, если условия проверки значения во всех тегах c:when ложно.

6.2.4. Тег url

Тег <c:url> позволяет создать адрес относительно корня приложения. Этот тег может применяться, например, при создании ссылок.

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>JSTL in JSP</title>
</head>
<body>
    <a href='<c:url value="/edit" />'><c:url value="/edit" /></a>
</body>
</html>

Параметр value содержит часть адреса, которая добавляется к корню приложения.

url in JSTL in JSP

6.2.5. redirect

С помощью тега redirect можно установить редирект на другой адрес. Например, в случае если атрибут val не определен, то делаем редирект на страницу notfound.jsp:

<c:if test="${val == null}">
    <c:redirect url="/notfound.jsp" />
</c:if>

7. Jakarta Expression Language

7.1. Expression Language

Expression Language или сокращенно EL предоставляет компактный синтаксис для обращения к массивам, коллекциям, объектам и их свойствам внутри страницы jsp. Он довольн прост. Вставку окрывает знак $, затем в фигурные скобки {} заключается выводимое значение:

${attribute}
${object.property}

7.1.1. Поиск данных

Откуда эти данные берутся? EL пытается найти значения для этих данных во всех доступных контекстах.

И EL просматривает все эти контексты в следующем порядке:

  1. Контекст страницы (данные сохраняются в PageContext)

  2. Контекст запроса

  3. Контекст сессии

  4. Контекст приложения

Соответственно, если контексты запроса и сессии содержат атрибут с одним и тем же именем, то будет использоваться атрибут из контекста запроса.

Затем найденное значение (если оно было найдено) конвертируется в строку и выводится на страницу.

Следует отметить, что мы не можем определить данные на странице, например, с помощью скриплета и затем вывести через EL:

<%
    String name = "Tom";
%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>JSP Application</title>
</head>
<body>
    <p>Name: ${name}</p>
</body>
</html>

Такой способ не будет работать. Если мы хотим определить данные непосредственно на страницы, то их затем необходимо включить в контекст страницы, который доступен через переменную pageContext:

<%
    pageContext.setAttribute("name", "Tom");
%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>JSP Application</title>
</head>
<body>
    <p>Name: ${name}</p>
</body>
</html>

Однако может сложиться ситуация, что сразу в нескольких контекстах одновременно содержатся данные с одним и тем же именем, например, name. Соответственно EL будет получать данные в порядке просмотра контекстов. Но, возможно, нам захочется выводить данные из какого-то определенного контекста. В этом случае перед названием данных мы можем указать название контекста: pageScope, requestScope, sessionScope или applicationScope. Например:

<%
    pageContext.setAttribute("name", "Bob");
%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>JSP Application</title>
</head>
<body>
    <p>Name: ${requestScope.name}</p>
</body>
</html>

7.1.2. Передача сложных объектов

Подобным образом мы можем передать и более сложные данные - списки, массивы, сложные объекты. Допустим, в сервлете на страницу передается массив:

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String[] users = new String[] {"Tom", "Bob", "Sam"};
        request.setAttribute("users", users);
        getServletContext().getRequestDispatcher("/basic.jsp").forward(request, response);
    }
}

И на странице basic.jsp получаем переданные данные:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
    <title>JSP Application</title>
</head>
<body>
    <p>Second: ${users[1]}</p>
    <p>Third: ${users[2]}</p>
</body>
</html>

Вместо массива в сервлете мы могли бы передать более гибкий объект - ArrayList:

ArrayList<String> users = new ArrayList<String>();
Collections.addAll(users, "Tom", "Bob", "Sam");
request.setAttribute("users", users);

В этом случае в jsp мы могли бы получить эти данные так же, как и из массива.

7.2. Встроенные объекты Expression Language

По умолчанию Expression Language предоставляет ряд встроенных объектов, которые позволяют использовать различные аспекты запроса:

  • param объект, который хранит все переданные странице параметры

  • paramValues хранит массив значений для определенного параметра (если для параметра передается сразу несколько значений)

  • header хранит все заголовки запроса

  • headerValues предоставляет массив значений для определенного заголовка запроса

  • cookie предоставляет доступ к отправленным в запросе кукам

  • initParam возвращает значение для определенного параметра из элемента context-param из файла web.xml

  • pageContext предоставляет доступ к объекту PageContext, который представляет контекст текущей страницы jsp

7.2.1. Получение параметров запроса

Определим следующую страницу postuser.jsp:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>User Info</title>
    </head>
    <body>
        <p>Name: ${param.name}</p>
        <p>Age: ${param.age}</p>
    </body>
</html>

Через объект param здесь получаем из запроса значения параметров name и age. Значения для параметров можно передать как через строку запроса, так и через отправку формы. Например, передача через строку запроса:

Expression Language in JSP

Также мы можем определить форму, например:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>User Form</title>
</head>
<body>
    <form action="postuser.jsp" method="post">
        Name: <input name="name" />
        <br><br>
        Age: <input name="age" type="number" min=1 />
        <br><br>
        <input type="submit" value="Submit" />
    </form>
</body>
</html>

И через эту форму передать данные в jsp:

Params in Expression Language

Если с одним параметром связано несколько значений, то можно использовать объект paramValues. Например, на станице html есть форма, где пользователь может ввести несколько своих телефонов:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>User Form</title>
</head>
<body>
    <form action="postuser.jsp" method="post">
        Name: <input name="name" />
        <br><br>
        Main Phone: <input name="phone" />
        <br><br>
        Additional Phone: <input name="phone" />
        <br><br>
        <input type="submit" value="Submit" />
    </form>
</body>
</html>

И на странице postuser.jsp мы можем получить все введенные телефоны через paramValues:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>User Info</title>
</head>
<body>
    <p>Name: ${param.name}</p>
    <p>Phone 1: ${paramValues.phone[0]}</p>
    <p>Phone 2: ${paramValues.phone[1]}</p>
</body>
</html>

После paramValues идет название параметра, причем параметр фактически представляет массив, и через индексы мы можем получить отдельные введенные данные.

ParamValues in JSP in Java EE

7.2.2. Получение куки

Допустим, в сервлете устанавливаются куки:

public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        Cookie cookie = new Cookie("userName", "Sam");
        response.addCookie(cookie);
    }
}

Получение этой куки в jsp:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Java Server Page</title>
</head>
<body>
    <div>
        <p>From cookie: ${cookie.userName.value}</p>
    </div>
</body>
</html>

7.2.3. Получение заголовков HTTP

Получим даные о юзер-агенте пользователя:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>User Info</title>
    </head>
    <body>
        <p>User-Agent: ${header["User-Agent"]}</p>
        <p>Host: ${header.host}</p>
    </body>
</html>

Если название заголовка представляет сложное слово с дефисами, как User-Agent, то для получения его значения используется конструкция header["название_заголовка"]

8. JavaBean

8.1. JavaBeans

Класс JavaBean должен соответствовать ряду ограничений:

  • иметь конструктор, который не принимает никаких параметров

  • определять для всех свойств, которые используются в jsp, методы геттеры и сеттеры

  • названия геттеров и сеттеров должны соответствовать условностям: перед именем переменной добавляется get (для геттера) и set (для сеттера), а название переменной включается с большой буквы. Например, если переменная называется firstName, то функции геттера и сеттера должны называться соответственно getFirstName() и setFirstName().

  • для переменных типа boolean для функции геттера используется вместо get приставка is. Например, переменная enabled и геттер isEnabled().

  • реализовать интерфейс Serializable или Externalizable

Рассмотрим, как использовать классы JavaBean. Допустим, у нас есть следующая структура:

Project structure

В папке Java Resources/src расположен класс User со следующим кодом:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 2041275512219239990L;

    private String name;
    private int age;

    public User() {
        this.name = "";
        this.age = 0;
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Данный класс представляет пользователя и является классом JavaBean: он реализует интерфейс Serializable, имеет конструктор без параметров, а его методы - геттеры и сеттеры, которые предоставляют доступ к переменным name и age, соответствуют условностям.

В папке WebContent определена страница user.jsp. Определим в ней следующий код:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>User Java Bean Page</title>
</head>
<body>
    <div>
		<p>Name: ${user.name}</p>
		<p>Age: ${user.age}</p>
    </div>
</body>
</html>

Данная страница jsp получает извне объект user и с помощью синтаксиса EL выводит значения его свойств. Стоит обратить внимание, что здесь идет обращение к переменным name и age, хотя они являются приватными.

В папке Java Resources/src в файле HelloServlet.java определен сервлет HelloServlet:

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        User tom = new User("Tom", 25);
        request.setAttribute("user", tom);
        getServletContext().getRequestDispatcher("/user.jsp")
                .forward(request, response);
    }
}

Сервлет создает объект User. Для передачи его на страницу user.jsp устанавливается атрибут user через вызов request.setAttribute("user", tom). Далее происходит перенаправление на страницу user.jsp. И, таким образом, страница получит данные из сервлета.

Page JavaBean

8.2. Пример Enterprise JavaBeans

Рассмотрим простой пример использования компонента Enterprise JavaBeans с выводом стандартного сообщения типа Hello World. Для этого создадим три проекта:

  • slon-app - проект enterprise (Enterprise Application Project), связывающий модули EJB с приложениями WEB

  • slon-module - проект с компонентами EJB

  • slon-web - WEB приложение, использующее компоненты EJB

Разработку будем вести в среде Eclipse, которая предоставляет Wizard для одновременного создания подобных проектов.

8.2.1. Создание проектов

Выбираем Wizard и создаем структуры проектов:

  1. FileNewOther (Ctrl+N). В раскрывшемся окне выбираем Java EE, Enterprise Application Project, жмем Next и переходим к следующему шагу.

  2. Определяем наименование проекта slon-app, Target runtime (использую JBoss 7.1) и к следующему шагу (Next).

  3. На этом шаге в открывшемся окне можно выбрать модули из существующих или создать новые. Создаем новые - нажимаем на New module …​.

  4. В открывшемся окне New Java EE ModuleEJB moduleWeb module. Определяем наименования как slon-module и slon-web. Нажимаем на кнопку Finish, после чего окно закрывается, создаются новые проекты (модули), которые появляются в списке проектов. Устанавливаем флаг Generate application.xml deployment descriptor и завершаем разработку структуры проектов (жмем Finish).

Не прилагая чрезмерных усилий создали проект slon-app типа Enterprise Application Project, включающий два проекта. На следующем скриншоте представлена структура созданных проектов в IDE Eclipse.

Структуры проектов приложения Enterprise Application Project

Приложение slon-app включает только файл конфигурации приложения application.xml, в котором определены модули.

8.2.2. Листинг application.xml

<?xml version="1.0" encoding="UTF-8"?>
<application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xmlns="http://java.sun.com/xml/ns/javaee"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
               http://java.sun.com/xml/ns/javaee/application_6.xsd"
             id="Application_ID" version="6">
    <display-name>slon-app</display-name>
    <module>
        <web>
            <web-uri>slon-web.war</web-uri>
            <context-root>slon-web</context-root>
        </web>
    </module>
    <module>
        <ejb>slon-module.jar</ejb>
    </module>
</application>

8.2.3. Создание компонента EJB

Компонент EJB создаем в модуле slon-module. Для этого включаем в проект интерфейс IHelloWorld.java (NewInterface) и класс HelloWorldBean (NewClass), представляющий сеансовый компонент EJB.

Листинг интерфейса IHelloWorld
package modules;

import javax.ejb.Local;

@Local
public interface IHelloWorld {
    String sayHello(String name);
}

Интерфейс IHelloWorld.java включает только один метод sayHello() и помечен аннотацией @Local.

Note
Аннотация @Local к интерфейсу сообщает контейнеру, что реализация IHelloWorld может быть доступна локально, посредством интерфейса. В этом есть определенный смысл, если интерфейс и использующие его компоненты EJB находятся в одном и том же приложении. Аннотацию @Local можно опустить; в этом случае интерфейс все равно будет интерпретироваться контейнером как локальный.
Листинг компонента EJB
package modules;

import javax.ejb.Stateless;

// Сеансовый компонент без сохранения состояния
@Stateless
public class HelloWorldBean implements IHelloWorld {
    @Override
    public String sayHello(String name) {
        return String.format("Hello %s, welcome to EJB 3.1!", name);
    }
}

В листинге HelloWorldBean.java представлен законченный, действующий компонент EJB, являющийся обычным Java-классом. Аннотация @Stateless обеспечивает преобразование простого Java-объекта (POJO) в полноценный сеансовый компонент EJB без сохранения состояния. Фактически, аннотации несут в себе дополнительную информацию в «форме комментария», которая добавляется в код.

Note
Аннотация @Stateless сообщает контейнеру EJB, что HelloWorldBean является сеансовым компонентом без сохранения состояния. Контейнер приложений автоматически добавит в компонент поддержку многопоточной модели выполнения, транзакций и возможность размещения в пулах. Поддержка многопоточности и транзакций гарантируют возможность использования любых ресурсов, таких как база данных или очередь сообщений, без необходимости разработки кода для обслуживания «конкуренции» или транзакций. Поддержка размещения в пулах гарантирует надежность даже при очень высоких нагрузках. При необходимости в компонент можно также добавить поддержку дополнительных служб EJB. Например: безопасность, планирование и интерцепторы.

8.2.4. Аннотации в EJB

Необходимо несколько слов сказать об использовании аннотаций при создании компонентов EJB. Аннотации появились в Java SE 5. До их появления единственным средством определения конфигурации приложений были файлы XML. Однако, использование файлов XML влечет за собой множество проблем:

  • формат XML чересчур многословен, трудно читаем и в нем очень легко допустить ошибку;

  • XML никак не поддерживает одну из сильнейших сторон Java – строгий контроль типов;

  • конфигурационные файлы XML часто получаются монолитными и они отделяют информацию о настройках от программного кода, использующего ее, что усложняет сопровождение.

Аннотации были созданы специально, чтобы ослабить эти проблемы. EJB 3 стала первой распространенной технологией на основе языка Java, проложившей путь к применению аннотаций. С тех пор по ее стопам пошли многие другие технологии типа JPA, JSF, Servlets, JUnit, Spring и т.д.

Благодаря нацеленности на простоту использования в EJB 3 применение аннотаций преобладает над применением конфигурации объектов в формате XML.

Основные аннотации EJB 3
  • @EJB — помечается используемый в классе компонент EJB;

  • @Stateless — определяется stateless session bean;

  • @Stateful — определяется stateful session bean;

  • @Singleton — определяется singleton session bean;

  • @Local — определяется local session bean;

  • @LocalBean — определяется bean, который будет использован локально, следовательно, его не нужно сериализовать;

  • @Remote — компонент доступен через RMI (Remote Method Invocation);

  • @Remove — помеченный данной аннотацией метод говорит контейнеру, что после его исполнения нет больше смысла хранить компонент EJB, т.е. его состояние сбрасывается;

  • @Entity — аннотация говорит контейнеру, что класс будет сущностью БД.

Таким образом, аннотации компонентов EJB 3 можно рассматривать по сути как параметры настройки, присваивающие фрагментам кода (объявление класса или метода) определенные атрибуты. Когда контейнер EJB обнаруживает эти атрибуты, он добавляет соответствующие им службы. Этот подход иначе называют «декларативным программированием» - разработчик указывает, что должно быть сделано, а система добавляет необходимый код.

8.2.5. Создание WEB приложения

В WEB-приложении slon-web нам необходимо обратиться к компоненту EJB, определенному в другом приложении и размещенном в контейнере приложений JBoss (который я использую). Для этого создадим сервлет ServletHello с компонентом EJB, и страницу index.jsp, из которой отправим запрос сервлету.

Создаем сервлет NewServlet. В сервлете определяем компонент EJB hello типа IHelloWorld с аннотацией @EJB. Контейнер приложений сам инициализирует переменную hello и свяжет ее с компонентом HelloWorldBean, реализующим интерфейс IHelloWorld.

Определение зависимости

На этапе разработки IDE Eclipse не определит тип интерфейса IHelloWorld. Поэтому необходимо к проекту slon-web подключить проект slon-module. Для этого откройте свойства проекта Properties и в Deployment Assembly подключите (Add …​) проект slon-module.

Листинг ServletHello.java
package servlets;

import java.io.IOException;

import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import modules.IHelloWorld;

public class ServletHello extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @EJB
    private IHelloWorld hello;

    @Override
    protected void doPost(HttpServletRequest request,
                          HttpServletResponse response)
                          throws ServletException, IOException {
        try {
            String answer = hello.sayHello(request.getParameter("name"));
            request.getSession().setAttribute("answer", answer);
            RequestDispatcher rd;
            rd = request.getRequestDispatcher("index.jsp");
            rd.forward(request, response);
        } catch (Exception e) {
            throw new ServletException(e.getMessage());
        }
    }
}

В сервлете переопределен метод doPost(). В методу вызывается функция sayHello() компонента EJB, которая возвращает ответ. Сервлет размещает ответ в странице index.jsp.

Следует обратить внимание, что компонент EJB (hello) не инициализируется, а сразу же вызывается его метод.

8.2.6. Листинг index.jsp

Создаем страницу index.jsp NewJSP File

<%@ page language="java" contentType="text/html; charset=UTF-8"
                         pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
                      "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>EJB 3.1</title>
</head>
<body>
    <h3>Enterprise JavaBeans 3</h3>
    <p>${answer}</p>
    <form action="sayHello" method="post">
        Введите имя : <input type="text" name="name" value=""/>
        <input type="submit" value="OK"/>
    </form>
</body>
</html>

На странице index.jsp определяем поле для ответа answer и форму запроса (form). При нажатии на кнопку OK будет выполнено действие sayHello(), определенное в дескрипторе приложений web.xml, которое вернет ответ от сервера (сервлет → EJB).

8.2.7. Листинг web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version= "2.5"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>Servlet Hello</display-name>
    <servlet>
        <servlet-name>ServletHello</servlet-name>
        <servlet-class>servlets.ServletHello</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ServletHello</servlet-name>
        <url-pattern>/sayHello</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

В дескрипторе приложения определен сервлет и действие (pattern sayHello), по которому он будет вызван.

На этом можно сказать разработка закончилась. Теперь осталось запустить приложение slon-app на исполнение (Run AsRun on Server) и увидеть следующую картинку в браузере.

Интерфейс страницы JSP

Note
Скриншот снят после ввода имени Alex и нажатия на кнопку OK, т.е. после получения ответа от сервера.

8.2.8. Сервер приложений

После запуска приложения slon-app также стартуют 2 приложения:

  • slon-module

  • slon-web

В логах сервера приложений JBoss увидим следующую информацию:

JNDI bindings for session bean named HelloWorldBean in deployment unit
              subdeployment "slon-module.jar" of deployment "slon-app.ear"
              are as follows:


java:global/slon-app/slon-module/HelloWorldBean!modules.IHelloWorld
java:app/slon-module/HelloWorldBean!modules.IHelloWorld
java:module/HelloWorldBean!modules.IHelloWorld
java:global/slon-app/slon-module/HelloWorldBean
java:app/slon-module/HelloWorldBean
java:module/HelloWorldBean


JNDI bindings for session bean named HelloWorldBean in deployment unit
              subdeployment "slon-web.war" of deployment "slon-app.ear"
              are as follows:


java:global/slon-app/slon-web/HelloWorldBean!modules.IHelloWorld
java:app/slon-web/HelloWorldBean!modules.IHelloWorld
java:module/HelloWorldBean!modules.IHelloWorld
java:global/slon-app/slon-web/HelloWorldBean
java:app/slon-web/HelloWorldBean
java:module/HelloWorldBean

Таким образом, сервер приложения «принял» наш компонент EJB.

8.3. POJI, POJO и JavaBean.

8.3.1. POJI.

Является аббревиатурой от Plain Old Java Interface, который соответствует стандартному интерфейсу Java, что означает, что эти интерфейсы находятся в контексте предоставления услуг в JEE. Например, услуга OSGI предлагается через POJI в JEE.

Другими словами, мы можем сказать, что POJI — это обычный интерфейс без какой-либо специальности, которая не унаследована от какого-либо специального API-интерфейса технологии или интерфейсов платформы.

interface myCustomInterface {
    public void myMethod();
}
interface mySecondCustomInterface extends myCustomInterface {
    public void mySecondMethod();
}

Оба интерфейса будут называться POJI, поскольку они не наследуют какой-либо технологический интерфейс.

interface GFG1 extends java.rmi.Remote {
    public void myMethod();
}
interface GFG2 extends java.beans.AppletInitializer {
    public void mySecondMethod();
}

Оба интерфейса не будут называться POJI, поскольку они наследуют специфический для технологии интерфейс.

8.3.2. POJO.

Как мы знаем, в Java POJO ссылается на простой старый объект Java. POJO и класс JavaBean в Java имеют некоторые общие черты, а именно:

Оба класса должны быть общедоступными, т.е. доступными для всех.

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

  • Оба класса должны иметь конструктор по умолчанию, т.е. не иметь аргумента конструктора.

  • public getters и setters должны присутствовать в обоих классах для доступа к переменным/свойствам.

  • Единственное различие между обоими классами состоит в том, что Java делает объекты JavaBean сериализованными, чтобы в случае необходимости можно было сохранить состояние класса JavaBeans. Поэтому из-за этого класс JavaBeans должен реализовывать интерфейс Serializable или Externalizable.

В связи с этим указывается, что все JavaBean-компоненты являются POJO, но не все POJO-объекты являются JavaBean-компонентами.

Пример класса JavaBean:

public class Employee implements java.io.Serializable {
    private int id;
    private String name;

    public Employee() {
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Пример класса POJO:

public class Employee {
    String name;
    public String id;
    private double salary;

    public Employee(String name, String id,double salary) {
        this.name = name;
        this.id = id;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }

    public Double getSalary() {
        return salary;
    }
}

9. Служба имен и каталогов JNDI

JNDI (Java Naming and Directory Interface) - это API для доступа к службам имен и каталогов. Службой имен, в самом широком смысле, называют систему, управляющую отображением множества имен во множество объектов. Зная имя объекта в системе, можно получить доступ к этому объекту или ассоциировать с этим именем другой объект. Самым наглядным примером JNDI является служба доменных имен DNS, которая определяет соответствие между понятными доменными именами (например, localhost) и сетевым IP-адресом (127.0.0.1). Имея определенное доменное имя DNS, мы может узнать соответствующий ему IP-адрес.

В службе каталогов именованные объекты сгруппированы в древовидную структуру. Кроме того объекты каталога имеют атрибуты. Наиболее близким и понятным примером такой службы является файловая система. Объекты файловой системы - файлы - собраны в каталоги и идентифицируются путями, например, C:\windows\notepad.exe. У файлов есть атрибуты: скрытый, архивный, только для чтения и другие. Передавая файловой системе путь, можно получить содержимое соответствующего файла, записать в него какие-то данные, изменить его атрибуты.

JNDI предназначен для единообразного доступа к разнообразным службам имен и каталогов, включая упомянутые выше DNS и файловую систему, а также LDAP, DataSource. Разные службы каталогов интегрируются с JNDI через интерфейс поставщика услуг SPI (Service Provider Interface).

Концепция JNDI основана на двух основных определениях :

  • ассоциация (binding) - соответствие JNDI-имени определенному объекту;

  • контекст (context) - среда, в которой хранится набор ассоциаций между объектами и именами.

По сути, JNDI представляет собой аналог JDBC для служб имен и каталогов. Также, как и JDBC предоставляет стандартный Java API для доступа к разнообразным базам данных, JNDI стандартизует доступ к службам имен и каталогов. На следующем рисунке представлена архитектура JNDI.

JNDI

Как показано на рисунке JNDI представляет обобщенную абстракцию доступа к самым разным службам имен, таким как:

  • LDAP (Lightweight Directory Access Protocol)

  • DNS (Domain Naming System)

  • NIS (Network Information Service)

  • NDS (Novell Directory Services)

  • RMI (Remote Method Invocation)

  • CORBA (Common Object Request Broker Architecture)

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

9.1. Инициализация контекста JNDI

Чтобы воспользоваться хранящимся в контексте JNDI ресурсом, необходимо инициализировать контекст javax.naming.Context и найти требуемый ресурс. Инициализация Context напоминает настройку драйвера JDBC при подключении к серверу базы данных.

Чтобы подключиться к службе имен или каталогов, необходимо получить библиотеки JNDI для этой службы. Это напоминает выбор соответствующего драйвера JDBC. Если требуется подключиться к LDAP, DNS или файловой системе компьютера, необходимо получить провайдера для соответствующей службы соответственно.

При работе в окружении Java EE (WEB), сервер приложений загружает все необходимые библиотеки для доступа к окружению JNDI. В противном случае необходимо настроить свое приложение, указав, какие библиотеки JNDI оно должно использовать. Сделать это можно, например, создав объект Properties и передав его конструктору InitialContext:

String CONTEXT = "oracle.j2ee.rmi.RMIInitialContextFactory";
String URL = "ormi://<host>:<port>/app";
String LOGIN = "SCOTT";
String PASSWORD = "TIGER";

Properties properties = new Properties();

properties.put(Context.INITIAL_CONTEXT_FACTORY, CONTEXT );
properties.put(Context.PROVIDER_URL , URL );
properties.put(Context.SECURITY_PRINCIPAL , LOGIN );
properties.put(Context.SECURITY_CREDENTIALS , PASSWORD);

Context context = new InitialContext(properties);

В представленном примере выполняется настройка объекта Properties для доступа к дереву JNDI удаленного сервера БД Oracle. Необходимо принять во внимание, что параметры подключения к JNDI зависят от конкретного производителя и представленный пример не является универсальным. Следует обращаться к документации с описанием своего сервера приложений, чтобы узнать, как организовать удаленное подключение.

Другой способ определения настроек JNDI связан с созданием файла jndi.properties и определением пути в CLASSPATH приложения. Содержимое файла настроек представляет те же самые пары имя/значение, что помещаются в объект Properties. После этого данный файл будет использоваться автоматически при создании контекста вызовом InitialContext:

Context context = new InitialContext();

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

Свойство

Описание

Пример

java.naming.factory.initial Context.INITIAL_CONTEXT_FACTORY

Имя фабричного класса, который будет использоваться для создания контекста.

oracle.j2ee.rmi.RMIInitialContextFactory

java.naming.provider.url Context.PROVIDER_URL

Адрес URL службы JNDI - определение расположения службы (формат описания URL зависит от службы).

ormi://localhost:98765/chapter1

java.naming.security.principal Context.SECURITY_PRINCIPAL

Идентификационная информация (учетная запись), позволяющая аутентифицировать вызывающую программу в службе JNDI.

SCOTT

java.naming.security.credentials Context.SECURITY_CREDENTIALS

Используемый для аутентификации пароль пользователя.

TIGER

javax.naming.Context.INITIAL_CONTEXT_FACTORY - определяет класс, который будет формировать экземпляр JNDI Context. Если работаете с DNS, то необходимо указывать класс, который создает Context для взаимодействия с DNS сервером (см. пример ниже).

9.2. Поиск ресурсов в JNDI, lookup

Метод lookup() интерфейса javax.naming.Context` возвращает ресурс с именем name, который следует привести к требуемому типу.

Object lookup (String name)

Если в аргументе name передать пустую строку, то возвращается новый экземпляр Context.

Чтобы отыскать ресурс, необходимо знать его имя. Допустим, что в JNDI компонент BidService зарегистрирован под именем /ejb/bid/BidService. Чтобы найти его, можно выполнить поиск непосредственно :

Context context = new InitialContext();
BidService service = (BidService) context.lookup("/ejb/bid/BidService");

или последовательно :

Context newContext = new InitialContext();
Context bidContext = (Context) newContext.lookup("/ejb/bid/");
BidService service = (BidService) bidContext.lookup("BidService");

9.3. Пример JNDI

Широкое распространение JNDI получила при подключении к серверу БД. Пример использования JNDI для настройки подключения DataSource к серверу базы Oracle в сервере приложений JBoss рассмотрен здесь.

Ниже приведен пример, в котором используется JNDI для просмотра содержимого корня контекста DNS сервера. В примере использован открытый адрес URL сайта Yandex, размещенный в Википедии.

import javax.naming.Context;
import javax.naming.NameClassPair;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.NamingEnumeration;

import java.util.Properties;

public class ExampleJNDI {
	static String DNS_CONTEXT = "com.sun.jndi.dns.DnsContextFactory";
	static String DNS_URL = "dns://77.88.8.8"; // yandex

	ExampleJNDI() {
		Properties props = new Properties ();
		props.put (Context.INITIAL_CONTEXT_FACTORY, DNS_CONTEXT);
		props.put (Context.PROVIDER_URL , DNS_URL );
		try {
			Context context = new InitialContext (props);
			NamingEnumeration<NameClassPair> names = context.list ("");
			while ( names.hasMoreElements ())
			System.out.println (names.nextElement());
		} catch (NamingException e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {
		new ExampleJNDI();
		System.exit(0);
	}
}

В результате работы программы в консоли можно увидеть следующие сообщения:

ltd: javax.naming.directory.DirContext
casino: javax.naming.directory.DirContext
alfaromeo: javax.naming.directory.DirContext
amsterdam: javax.naming.directory.DirContext
cat: javax.naming.directory.DirContext
car: javax.naming.directory.DirContext
photography: javax.naming.directory.DirContext
cam: javax.naming.directory.DirContext
aquarelle: javax.naming.directory.DirContext
theatre: javax.naming.directory.DirContext
media: javax.naming.directory.DirContext
total: javax.naming.directory.DirContext
diet: javax.naming.directory.DirContext
today: javax.naming.directory.DirContext
actor: javax.naming.directory.DirContext
fans: javax.naming.directory.DirContext
career: javax.naming.directory.DirContext
...

В Enterprise JavaBeans JNDI используется для нахождения всех видов ресурсов, включая компонент EJB, пул соединений с базой данных, информацию об окружении и многое другое. Таким образом, из окна контейнера EJB можно «увидеть остальной мир» посредством JNDI. Клиентское приложение также использует JNDI для получения соединения с фабрикой EJB.

Что представляет собой поставщик услуг при использовании JNDI с EJB? Ответ заключается в том, что контейнер EJB определяет свою собственную службу JNDI, специализированную для работы с ресурсами, управляемыми контейнером. Иными словами, служба JNDI в этом случае позволяет клиентам и Enterprise JavaBeans находить ресурсы по JNDI именам. Следует помнить, что когда вы стартует Ваш контейнер EJB, то также неявно запускается EJB JNDI служба, которая доступна через стандартный JNDI API. О формате именования компонентов EJB подробно представлено здесь.

10. Servlet Container and DataSource

Для подключения к БД из web-app, который развертывается на servlet container требуется несколько обязательных значений:

  • url

  • username

  • password

  • driver

Как же установить эти значения?

  • Указать в java-code
    Наихудшее решение из всех возможных, так как требует компиляции кода и упаковки приложения в случае изменения этих значений. Изменение этих значений происходит как минимум под каждое новый environment (окружение), например: dev, stage, prod. В то же самое время это наиболее небезопасный вариант, так как доступ к конфигурации имеют все, кто имеет доступ к source code (исходному коду).

  • Указать в как ресурс в web-app
    Отличает от предыдущего только то, что не нужно производить компиляцию кода, так значения указаны либо в каком-то конфигурационном файле, либо в web.xml

  • Указать как JNDI ресурс в servlet container
    Из положительных моментов конфигурация не находится в source code. С точки зрения безопасности, доступ имеет только тот, кто имеет доступ к OS, где находится servlet container, и соответствующие права на редактирование конфигурации servlet container.

  • Указать в собственном конфигурационном файле
    Из положительных моментов конфигурация не находится в source code. С точки зрения безопасности, доступ имеет только тот, кто имеет доступ к OS, где находится servlet container, и соответствующие права на редактирование конфигурационного файла.

  • Указать как var env (переменные среды)
    Из положительных моментов конфигурация не находится в source code. С точки зрения безопасности, доступ имеет только тот, кто имеет доступ к OS, где находится servlet container, и соответствующие права на редактирование var env.

10.1. Как JNDI ресурс в servlet container

10.1.1. Добавить JNDI ресурс для web-app

Например, если host называется myHost и приложение, которое представляет собой .war-file, называется myApp.war, тогда необходимо создать файл YOUR_TOMCAT_HOME/conf/Catalina/myHost/myApp.xml. Этот файл будет иметь следующее содержимое:

<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Resource
        name="jdbc/postgres"
        auth="Container"
        type="javax.sql.DataSource"
        driverClassName="org.postgresql.Driver"
        url="jdbc:postgresql://127.0.0.1:5432/interview_helper"
        username="postgres"
        password="12345678"
        maxTotal="20"
        maxIdle="10"
        maxWaitMillis="-1"
    />
</Context>
  • name - имя ресурса, которое можно указать самостоятельно, по нему и будет происходить доступ к ресурсу

10.1.2. Описать ресурс в дескрипторе web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    ...

    <resource-ref>
        <description>PostgreSQL Datasource</description>
        <res-ref-name>jdbc/postgres</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>
</web-app>
  • description - описание ресурса, которое можно указать самостоятельно, для понимания того, какую роль выполняет ресурс

10.1.3. Получение ресурса в коде

public class Program {
    public static void main(String[] args) {
        InitialContext initialContext = new InitialContext();
        Context context = (Context) initialContext.lookup("java:comp/env");
        DataSource dataSource = (DataSource) context.lookup("jdbc/postgres");
    }
}

11. Contexts and Dependency Injection (CDI)

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

11.1. Вступление

Данный небольшой обзор хочется посвятить такой штуке, как CDI. Что это? CDI — это Contexts and Dependency Injection. Это спецификация Java EE, описывающая внедрение зависимостей (Dependency Injection) и контексты. Для информации можно посмотреть на сайт http://cdi-spec.org/.

Так как CDI — это спецификация (описание того, как оно должно работать, набор интерфейсов), то для использования нам понадобится и реализация. Одной из таких реализаций является Weld — http://weld.cdi-spec.org/. Для управления зависимостями и создания проекта воспользуемся Maven — https://maven.apache.org/

Итак, Maven уже установлен, теперь будем разбираться сразу на практике, чтобы не разбираться в абстрактном. Для этого при помощи Maven создадим проект. Откроем командную строку (в Windows можно при помощи Win+R вызвать окно Выполнить и выполнить cmd) и попросим Maven всё сделать без нашего участия. Для этого у Maven есть такое понятие, как archetype: Maven Archetype.

mvn archetype:generate

После этого на вопросах Choose a number or apply filter и Choose org.apache.maven.archetypes:maven-archetype-quickstart version просто нажимаем Enter. Далее вводим идентификаторы проекта, так называемые GAV (см. Naming Convention Guide).

Entering GAV properties

После успешного создания проекта увидим надпись "BUILD SUCCESS". Теперь можно открывать наш проект в любимой IDE.

11.2. Добавление CDI в проект

Напоминаю, что у CDI есть интересный сайт — http://www.cdi-spec.org/.

cdi-spec.org download
Таблица в разделе download, которая содержит необходимые данные

В таблице можно подсмотреть, как для Maven описывается тот факт, что используется в проекте API для CDI. API - это application programming interface, то есть некоторый программный интерфейс. Работая с интерфейсом, можно не переживать о том, что и как за этим интерфейсом работает. API представляет собой некоторый jar архив, который можно начать использовать в своём проекте, то есть проект начинает зависеть от этого jar. Следовательно, CDI API для проекта зависимость, dependency.

В Maven проект описывается в файлах POM.xml (POM — Project Object Model). Зависимости описываются в блоке dependencies.

В блок dependencies добавляется новая запись

<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>

Как можно заметить, scope не указывается со значением provided. Почему такое отличие? Такой scope означает, что зависимость предоставит кто-то. Когда приложение работает на Java EE сервере, то это означает что сервер предоставит приложению все необходимые JEE технологии. Для простоты данного обзора будем работать в Java SE окружении, следовательно, никто не предоставит данную зависимость. Подробнее про Dependency Scope можно прочитать тут: Dependency Scope.

Хорошо, теперь есть возможность работать с интерфейсами. Но нужна и реализация. Напоминаю, использоваться будет Weld. Интересно, что везде приводятся разные зависимости. Но следуем документации 18.4.5. Setting the Classpath.

Прописываем dependencies, в соответствии с документацией

<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>

Важно, что версии Weld третьей линейки поддерживают CDI 2.0. Следовательно, можно рассчитывать на API этой версии. Теперь все готово к написанию кода.

11.3. Инициализация CDI контейнера

CDI — это механизм. Этим механизмом кто-то должен управлять. Как указывалось выше, то таким управляющим является контейнер. Следовательно, такой контейнер нужно создать.

Дописываем main() метод

public class Program {
    public static void main(String[] args) {
        SeContainerInitializer initializer = SeContainerInitializer.newInstance();
        initializer.addPackages(App.class.getPackage());
        SeContainer container = initializer.initialize();
    }
}

CDI контейнер создается вручную т.к. работа происходит в SE окружении. В обычных боевых проектах код выполняется на сервере, который предоставляет коду различные технологии. Соответственно, если сервер предоставляет CDI, то это значит, что на сервере уже есть CDI контейнер и не нужно будет ничего добавлять. Но для целей урока будет использоваться SE окружение.

Зачем использовать контейнер? Контейнер внутри содержит beans (CDI beans).

11.4. CDI Beans

Итак, beans. Что такое CDI bean? Это Java класс, который соответствует некоторым правилам. Эти правила описаны в спецификации, в главе 2.2. What kinds of classes are beans?.

Добавляем CDI bean в тот же пакет, где и класс App:

public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}

Вызываем bean из main() метода:

public class Program {
    public static void main(String[] args) {
        Logger logger = container.select(Logger.class).get();
        logger.print("Hello, World!");
    }
}

Как видно, bean не создавался при помощи ключевого слова new. Попросив у CDI контейнера: "CDI контейнер. Есть необходимость в экземпляре класса Logger, предоставь контейнер, пожалуйста". Такой способ называется Dependency lookup, то есть поиск зависимости.

Создаем новый класс:

public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}

Примитивный класс, возвращающий текстовое представление даты.

Добавляем вывод даты в сообщение:

public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}

Появилась интересная аннотация @Inject. Как сказано в главе 4.1. Injection points документации cdi weld, при помощи данной аннотации определяется Injection Point. На русском это можно прочитать как "точки внедрения", которые используются CDI контейнером, чтобы внедрять зависимости в момент инстанциирования beans.

Как видно, полю dateSource (источник даты) не присваивается никаких значений. Причиной тому тот факт, что CDI контейнер позволяет внутри CDI beans (только те beans, которые контейнер инстанциировал самостоятельно, т.е. beans управляемые нашим контейнером) использовать "Dependency Injection". Это другой способ Inversion of Control, подхода, когда зависимостью управляет кто-то другой, вместо явного создания объектов.

Внедрение зависимостей может быть выполнено через метод, конструктор или поле. Подробнее см. главу спецификации CDI 5.5. Dependency injection.

Процедура определения того, что нужно внедрять, называется typesafe resolution, о чём и следует поговорить.

11.5. Разрешение имени или Typesafe resolution

Обычно, в качестве типа внедряемого объекта используется интерфейс, а CDI контейнер самостоятельно определяет, какую реализацию нужно выбрать. Это полезно по многим причинам, которые будут рассматриваться далее.

Интерфейс Logger:
public interface Logger {
    void print(String message);
}

Данный интерфейс говорит, что есть некоторый logger, которому можно передать сообщение на выполнение задачи — logging. Как и куда — в данном случае неинтересно.

Создаем реализацию для logger:

public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}

Как видно, это logger, который пишет в System.out. Прекрасно. Теперь, main() метод отработает, как и раньше.

public class Program {
    public static void main(String[] args){
        Logger logger = container.select(Logger.class).get();
    }
}

Данная строка по-прежнему получит logger. И вся прелесть в том, что достаточно знать интерфейс, а о реализации вместо разработчика думает CDI контейнер.

Добавляем вторую реализацию, которая должна отправлять log на удалённое хранилище:

public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}

Запускаем код без изменений, получаем ошибку:

org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger

CDI контейнер видит у интерфейса две реализации и не может из них выбрать.

Что же делать? Существует несколько доступных вариаций. Самый простой — аннотация @Vetoed, которая передаст команду CDI контейнеру не воспринимать этот класс как CDI bean.

Но есть куда более интересный подход. CDI bean может быть помечен как "альтернатива" при помощи аннотации @Alternative, описанной в главе 4.7. Alternatives документации по Weld CDI.

Что это значит? Это значит, что пока явно не указывается, что нужно использовать CDI bean, ничего выбрано не будет. Это альтернативный вариант bean. Помечаем bean NetworkLogger как @Alternative, и код снова выполняется и используется SystemOutLogger.

Чтобы включить альтернативу должен появиться файл beans.xml. Может возникнуть вопрос: beans.xml, where do the developer put this file?.

correct CDI structure
Правильное размещение файла

Как только появляется данный файл, то артефакт с кодом будет называться Explicit bean archive.

Теперь существует 2 отдельных конфигурации: программная и xml. Проблема в том, что конфигурации будут загружать одинаковые данные. Например, определение bean DataSource будет загружено 2 раза и при выполнении программа упадёт, т.к. CDI контейнер будет думать про конфигурации как про 2 отдельных bean (хотя по факту это один и тот же класс, о котором CDI контейнер узнал дважды). Чтобы это избежать есть 2 варианта:

  • убрать строку

initializer.addPackages(App.class.getPackage())

Добавляем указание альтернативы в xml файл:

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
    <alternatives>
        <class>ru.javarush.NetworkLogger</class>
    </alternatives>
</beans>
  • добавить в корневой элемент beans атрибут bean-discovery-mode со значением "none" и указать альтернативу программно:

initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);

Таким образом при помощи альтернативы CDI контейнер может определять, какой bean выбрать. Интересно, что если CDI контейнер будет знать несколько альтернатив для одного и того же интерфейса, то можно дать подсказку CDI контейнеру, указав приоритет при помощи аннотации @Priority (Начиная с CDI 1.1).

11.6. Квалификаторы

Отдельно стоит обсудить такую вещь как квалификаторы. Квалификатор указывается аннотацией над bean и уточняют поиск bean. А теперь подробнее.

Интересно, что любой CDI bean в любом случае имеет как минимум один квалификатор — @Any.

Если не указать над bean НИ ОДИН квалификатор, но тогда CDI контейнер сам добавляет к квалификатору @Any ещё один квалификатор — @Default. Если же хоть что-то указать (например, явно указать`@Any`), то квалификатор @Default автоматически добавлен не будет.

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

Вводим Enum для типа протокола:

public enum ProtocolType {
    HTTP, HTTPS
}

Создаем квалификатор, который учитывает тип протокола

@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}

Стоит отметить, что поля, помеченные как @Nonbinding не влияют на определение квалификатора.

Теперь надо указать квалификатор. Указывается он над типом bean (чтобы CDI знал, как его определить) и над Injection Point (с аннотацией @Inject, чтобы понимать, какой bean искать для внедрения в этом месте).

Например, можно добавить какой-нибудь класс с квалификатором.

Делаем квалификаторы внутри NetworkLogger:

public interface Sender {
	void send(byte[] data);
}

@Protocol(ProtocolType.HTTP)
public static class HTTPSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sent via HTTP");
	}
}

@Protocol(ProtocolType.HTTPS)
public static class HTTPSSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sent via HTTPS");
	}
}

При выполнении Inject, указываем квалификатор, который будет влиять на то, какой именно класс будет использован:

@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;

Здорово, не правда ли?) Кажется, что красиво, но непонятно зачем.

Допускаем ситуацию

public class Program {
    public static void main(String[] args){
        Protocol protocol = new Protocol() {
            @Override
            public Class<? extends Annotation> annotationType() {
                return Protocol.class;
            }
            @Override
            public ProtocolType value() {
                String value = "HTTP";
                return ProtocolType.valueOf(value);
            }
        };
        container.select(NetworkLogger.Sender.class, protocol)
                .get()
                .send(null);
    }
}

Таким образом, можно переопределить получение значения value так, что значение может вычисляться динамически. Например, значение может браться из каких-нибудь настроек. Тогда можно менять реализацию даже на лету, без перекомпилирования или рестарта программы/сервера. Гораздо интереснее становится, не правда ли? )

11.7. Producers

Ещё одной полезной возможностью CDI являются Producers. Это особые методы (отмечены специальной аннотацией), которые вызываются, когда какой-то bean запросил внедрение зависимости. Подробнее описано в документации в разделе 2.2.3. Producer methods.

Самый простой пример
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}

Теперь при Inject в поля типа Integer будет вызван данный метод и из него будет получено значение. Тут стоит сразу понимать, что когда есть ключевое слово new, то надо сразу понимать, что это НЕ CDI bean. То есть экземпляр класса Random не станет CDI bean только потому, что он получен из чего-то, что контролирует CDI контейнер (в данном случае продюсер).

11.8. Interceptors

Interceptors — это такие перехватчики, "вклинивающиеся" в работу. В CDI это сделано довольно понятно. Давайте посмотрим, как можно выполнить logging при помощи Interceptors (или перехватчиков).

Описываем привязку к интерцептору при помощи аннотаций:

@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}

Тут главное, что это привязка для интерцептора (@InterceptorBinding), которая будет наследоваться при extends (@InterceptorBinding).

Напишем интерцептор:

@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}

Подробнее про то, как пишутся интерцепторы, можно прочитать в примере из спецификации: 1.3.6. Interceptor example.

Включаем интерцептор, указываем аннотацию binding над выполняемым методом

@ConsoleLog
public void print(String message) {
}

И теперь ещё очень важная деталь. Интерцепторы по умолчанию выключены и их надо включать по аналогии с альтернативами.

Включаем интерцепторы в файле beans.xml

<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>

11.9. Event & Observers

CDI предоставляет так же модель событий и наблюдателей. Тут не так всё очевидно, как с интерцепторами.

Итак, Event в данном случае может являться абсолютно любой класс, для описания ничего особого не надо.

Создаем
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}

Создаем объект, который ожидает наступление события

public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}

Здесь важно указать аннотацию @Observes, указывающая, что это не просто метод, а метод, который должен быть вызван как результат наблюдения за событиями типа LogEvent.

Создаем наблюдателя:

public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}

Теперь есть единственный метод, который будет говорить контейнеру, что случилось событие Event для типа события LogEvent.

Осталось только использовать наблюдатель. Например, в NetworkLogger можно добавить @Inject` нашего LogObserver:

@Inject
private LogObserver observer;

В методе print() уведомляем наблюдателя о появлении нового события:

public void print(String message) {
    observer.observe(new LogEvent());
}

Тут важно знать, что события можно обрабатывать в одном потоке и в нескольких. Для асинхронной обработки служит метод fireAsync() (вместо fire()) и аннотация @ObservesAsync (вместо @Observes). Например, если все события выполняются в разных потоках, то если 1 поток упадёт с Exception, то остальные смогут выполнить свою работу для других событий.

Подробнее про события в CDI можно прочитать, как обычно, в спецификации, в главе 10. Events.

11.10. Decorators

Как указывалось выше, под крылом CDI собраны различные паттерны проектирования. И вот ещё один — декоратор. Это очень интересная штука.

Взглянем на класс:

@Decorator
public abstract class LoggerDecorator implements Logger {
    public final static String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_RESET = "\u001B[0m";

    @Inject
    @Delegate
    private Logger delegate;

    @Override
    public void print(String message) {
        delegate.print(ANSI_GREEN + message + ANSI_RESET);
    }
}

Объявляя его декоратором, следует упомянуть, что когда будет использована какая-либо реализация Logger, то будет использоваться эта "надстройка", знающая настоящую реализацию, которая хранится в поле delegate (т.к. оно помечено аннотацией @Delegate).

Декораторы могут быть ассоциированы только с CDI bean, который сам не интерцептор и не декоратор.

Пример можно увидеть так же в спецификации: "1.3.7. Decorator example".

Включаем декоратор, как и интерцептор, в beans.xml:

<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>

Подробнее см. weld reference: Chapter 10. Decorators.

11.11. Жизненный цикл

Schema of Bean Lifecycle
Жизненный цикл beans

Как видно по картинке, есть так называемые lifecycle callbacks. Это аннотации, которые скажут CDI контейнеру вызывать определённые методы на определённом этапе жизненного цикла bean.

Пример
@PostConstruct
public void init() {
	System.out.println("Inited");
}

Такой метод будет вызывать при инстанциировании bean CDI контейнером. Аналогично будет и с @PreDestroy при уничтожении bean, когда необходимость в нем отпадет.

В аббревиатуре CDI не зря есть буква C - Context. Beans в CDI являются contextual, то есть их жизненный цикл зависит от контекста, в котором beans существуют внутри CDI контейнера. Чтобы в этом лучше разбираться стоит прочитать раздел спецификации 7. Lifecycle of contextual instances.

Так же стоит знать, что есть жизненный цикл и у самого контейнера, о чём можно прочитать в Container lifecycle events.

11.12. Итого

Выше рассмотрена самая верхушка айсберга под названием CDI, который является частью JEE спецификации и используется в Java EE окружении. Те, кто используют Spring - используют не CDI, а DI, то есть это несколько разные спецификации. Но зная и понимая вышеуказанное легко можно перестроиться. Учитывая, что Spring поддерживает аннотации из мира CDI (те же Inject).

Дополнительные материалы: