
Развитие информационных технологий все более и более вовлекает использование инфраструктуры Интернет. Распределенные и мобильные приложения все чаще используют обмен информацией по протоколу HTTP. При этом архитектура Клиент-Сервер остается самой распространённой и простой для освоения, создания и эксплуатации. Принцип архитектуры Клиент-Сервер прост — сервер предоставляет ресурс, а клиент использует этот ресурс.
Данная статья представляет собой попытку понятного описания создания простой веб-службы. Простой, практичный и детально описанный пример часто приносит больше пользы в изучении технологии нежели усердное чтение литературы. В статье рассматривается создание веб-службы простого калькулятора на основе REST, JSON, используя Eclipse и встроенной сервер Jetty.
Задача
Рассмотрим создание калькулятора как веб-службу, реализующую простые арифметические действия с двумя числами. Веб-службу можно рассматривать так же как и удалённую функцию, принимающую входные параметры и выдающую результат. Поэтому её функциональность можно описать следующим образом:
Входные параметры:
- a – первый аргумент;
- b – второй аргумент;
- op – арифметический оператор, выражаемый одним из знаков +, -, /, *.
Выходные параметры:
- error – первый аргумент;
- result – второй аргумент;
Пример запроса/ответа — сумма
Запрос: http://localhost:8080/func?a=8.78&b=4.15&op=+
Ответ:
{ “error”:0, “result”:12.93 }
Пример запроса/ответа — разность
Запрос: http://localhost:8080/func?a=8.78&b=4.15&op=-
Ответ:
{ “error”:0, “result”:4.63 }
Пример запроса/ответа — произведение
Запрос: http://localhost:8080/func?a=8.78&b=4.15&op=*
Ответ:
{ “error”:0, “result”:36.437 }
Пример запроса/ответа — частное
Запрос: http://localhost:8080/func?a=8.78&b=4.15&op=/
Ответ:
{ “error”:0, “result”:2.1156626506 }
Пример запроса/ответа – ошибка «деление на 0»
Запрос: http://localhost:8080/func?a=8.78&b=0&op=/
Ответ:
{ “error”:1 }
Пример запроса/ответа – ошибка «неверный формат числа»
Запрос: http://localhost:8080/func?a=8.78&b=4.15m&op=/
Ответ:
{ “error”:1 }
Установка библиотек Jetty
Jetty очень удобен для создания веб приложений. Использование его как встроенного сервера освобождает разработчика от развёртывания веб приложения на внешний сервер при ��аждом запуске. Также это не требует установку внешнего сервера приложений.
Для большинства случаев достаточно загрузить библиотеки сервера, зарегистрировать их в Eclipse как библиотеку пользователя и далее использовать ссылку на эту библиотеку. Этот подход прост для начинающих Java программистов так как не требует наличия и навыков инструментария автоматизации сборки, такого как Maven или Gradle.
Установить необходимые библиотеки Jetty в Eclipse можно следующим образом:
1. Загрузим сжатый файл по ссылке http://download.eclipse.org/jetty/ и распакуем его;
2. В корневой папке проектов ( обычно это Workspace ) создадим папку jars, а в ней папку jetty;
3. Скопируем содержимое папки lib из распакованного ранее файла в созданную папку jetty;
4. В меню Window/Preferences выберем раздел Java/Build Path/User Libraries.

5. Кликнем кнопку New…, введём имя библиотеки jetty и кликнем кнопку ОК.

6. Далее при выделенной только что созданной библиотеке jetty в окошке Preferences кликнем кнопку Add External JARs…. В окне JAR Selection выберем все JAR-файлы из ранее созданной папки jars/jetty.

7. В итоге JAR-файлы будут загружены в пользовательскую библиотеку jetty. Хотя файлы, находящиеся в под-папках не будут загружены, для большинства случаев в них нет необходимости.

Создание проекта веб сервера
В меню File/New выберем Dynamic Web Project. В поле Project name введём SCalculator. Нажмём кнопку Finish.


Добавление ссылки на библиотеку jetty
Сразу после создания проект не содержит ссылку на библиотеку jetty. Подключённые библиотеки можно просмотреть в Project Explorer во вкладке Java Resources, в под-вкладке Libraries.

Кликнем правой кнопкой мыши на метку проекта и в контекстном ��еню выберем Build Path и далее Configure Build Path…. Во вкладке Java Build Path на страничке Libraries кликнем кнопку Add Library….

Выберем User Library и кликнем Next. Выберем jetty и кликнем Finish.


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

Создание сервлета калькулятора
Создание файла сервлета
Сервлет калькулятора будет содержать весь код декодирования входных данных, вычисления, и формирования ответа. Для создания сервлета кликнем правой кнопкой мыши на наименование проекта в панели Project Explorer, в контекстном меню выберем New и далее Servlet. В название класса введём SrvltCalculator и кликнем кнопку Finish.

В панели Project Explorer можно увидеть созданный файл SrvltCalculator.java. Его содержимое автоматически открывается в редакторе.
Удаление лишнего кода
Для упрощения дальнейшего редактирования файлов удалим неиспользуемые конструктор сервлета SrvltCalculator и метод doPost.
Добавление импортируемых модулей
Код, который будет добавлен в файл сервлета потребует добавления следующих ниже строк кода включения модулей. Добавим эти строки.
import java.io.IOException; import java.io.UnsupportedEncodingException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
Добавление кода в метод doGet
Метод doGet содержит код обработки GET–запросов. В этом методе последовательно добавим приведённые ниже фрагменты кода.
Приём параметров в соответствующие строковые переменные.
String str_a = request.getParameter("a"); String str_b = request.getParameter("b"); String str_op = request.getParameter("op");
Объявление переменных для принятия декодированных из строковых переменных числовых параметров a и b.
double value_a = 0; double value_b = 0;
Объявление переменной контроля возникновения ошибки noError.
boolean noError = true;
Попытка декодирования числовых параметров a и b из соответствующих строковых переменных. При ошибке декодирования переменная noError принимает значение “ложь”.
try { value_a = Double.parseDouble(str_a); value_b = Double.parseDouble(str_b); } catch ( Exception ex ) { noError = false; }
Открытие секции кода для случая, если при декодировании числовых параметров ошибка не возникла.
if ( noError ) {
Объявление числовой переменной result для хранения результата.
double result = 0;
Открытие секции try для включения кода вычисления и контроля ошибок. Секция необходима, так как при арифметических операциях может возникнуть ошибка операции с плавающей запятой.
try {
Для случая операции сложения, вызываем функцию functionSum, которую опишем позднее.
if (str_op.equals("+")) result = functionSum( value_a, value_b ); else
Для случая операции вычитания, вызываем функцию functionDif, которую опишем позднее.
if (str_op.equals("-")) result = functionDif( value_a, value_b ); else
Для случая операции умножения, вызываем функцию functionMul, которую опишем позднее.
if (str_op.equals("*")) result = functionMul( value_a, value_b ); else
Для случая операции деления, вызываем функцию functionDiv, которую опишем позднее. Так как для типа double ошибка деления на ноль на современных платформах не возникает, ситуацию в которой делитель равен нулю мы контролируем вручную.
if (str_op.equals("/") && (value_b!=0)) result = functionDiv( value_a, value_b ); else
После проверки всех четырёх операций устанавливаем флажок отсутствия ошибки в “ложь”. Это делается для идентификации того, что арифметическая операция не идентифицирована.
noError = false;
Закрываем блок try с установлением флажка отсутствия ошибки в “ложь” в случае возникновения исключительной ситуации.
} catch ( Exception ex ) { noError = false; }
В случае если ошибки не возникло, отсылаем результат методом doSetResult, который опишем ниже. Так как работа метода doGet на этом завершается, возвращаемся оператором return.
if ( noError ) { doSetResult( response, result ); return; }
Закрываем секцию, начатую оператором “if ( noError ) {“:
}
Так как при обработке запроса где-то произошла ошибка и функция doGet не возвратила управление с успешным вычислением, возвращаем сообщение об ошибке методом doSetError, который опишем ниже.
doSetError( response );
Междоменные запросы
Междоменные запросы ( также такие запросы называются кроссдоменными / cross domain ) имеют место при запросах с веб страниц, расположенных вне сетевого домена обслуживающего сервера. Ответы на подобные запросы обычно блокируются для противостояния меж-доменным атакам. Для отключения блокировки в ответах сервера можно установить заголовок Access-Control-Allow-Origin:*.
Метод doSetResult
Метод doSetResult производит форматирование ответа и необходимую установку параметров HTTP ответа. Значение строк метода следующее:
- Первая строка формирует JSON ответ. Так как структура ответа проста, специализированная библиотека JSON не используется;
- Во второй строке JSON ответ кодируется в тело HTTP ответа в двоичный вид посредством кодировки UTF-8;
- В третьей строке указывается тип содержания тела ответа HTTP;
- В четвёртой строке устанавливается разрешение на междоменные запросы;
- В пятой строке устанавливается флажок OK HTTP ответа.
protected void doSetResult( HttpServletResponse response, double result ) throws UnsupportedEncodingException, IOException { String reply = "{\"error\":0,\"result\":" + Double.toString(result) + "}"; response.getOutputStream().write( reply.getBytes("UTF-8") ); response.setContentType("application/json; charset=UTF-8"); response.setHeader("Access-Control-Allow-Origin", "*"); response.setStatus( HttpServletResponse.SC_OK ); }
Метод doSetError
Метод doSetError производит форматирование ответа сообщения об ошибке и необходимую установку параметров HTTP ответа. Значение строк метода следующее:
- Первая строка формирует JSON ответ. Так как структура ответа проста, специализированная библиотека JSON не используется;
- Во второй строке JSON ответ кодируется в тело HTTP ответа в двоичный вид посредством кодировки UTF-8;
- В третьей строке указывается тип содержания тела ответа HTTP;
- В четвёртой строке устанавливается разрешение на междоменные запросы;
- В пятой строке устанавливается флажок OK HTTP ответа. Следует учесть, что сообщение содержит ошибку, связанную с арифметическими вычислениями. Так как эта ошибка не связана с протоколом HTTP, флажок статуса устанавливается в ОК.
protected void doSetError( HttpServletResponse response ) throws UnsupportedEncodingException, IOException { String reply = "{\"error\":1}"; response.getOutputStream().write( reply.getBytes("UTF-8") ); response.setContentType("application/json; charset=UTF-8"); response.setHeader("Access-Control-Allow-Origin", "*"); response.setStatus( HttpServletResponse.SC_OK ); }
Методы реализации арифметических операций
Архитектура рассматриваемого простого примера подразумевает разделение кода на функциональные части. Ввиду этого арифметические операции реализованы в виде отдельных функций, а не включены в тело метода doGet. Так как функции простые, их код комментировать не будем.
protected double functionSum( double a, double b ) { return a + b; } protected double functionDif( double a, double b ) { return a - b; } protected double functionMul( double a, double b ) { return a * b; } protected double functionDiv( double a, double b ) { return a / b; }
Исходный код программы можно найти в репозитории GitHub.
Создание основного класса
Основной класс приложения будет содержать функцию main – так называемую точку входа, с которой начинается работа программы. Функция main включит инициализацию, настройку и запуск встроенного сервера Jetty.
Для создания основного класса приложения кликнем правой кнопкой на наименовании проекта в панели Project Explorer, в контекстном меню выберем New и далее Class. В название класса введём Main. Установим флажок для создания статической функции main и кликнем кнопку Finish.

Так же как и в случае сервлета создаётся и открывается в текстовом редакторе соответствующий файл.
Добавление импортируемых модулей
Код, который будет добавлен в файл основного класса приложения потребует добавления следующих ниже строк кода включения модулей. Введём эти строки.
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder;
Добавление кода в метод main
Код метода main начинается с объявления переменной port и присваивания ей номера порта, который будет слушать сервер. Такой подход позволит быстро и легко изменить порт в случае необходимости в случае дальнейшего роста программы.
int port = 8080;
Создаем класс сервера.
Server server = new Server(port);
Указываем параметры, которые свяжут путь строки запроса с созданным выше сервлетом.
ServletContextHandler context = new ServletContextHandler( ServletContextHandler.SESSIONS ); context.setContextPath( "/" ); // http://localhost:8080/func context.addServlet(new ServletHolder( new SrvltCalculator( ) ),"/func");
Указываем серверу обработчик запросов.
HandlerList handlers = new HandlerList( ); handlers.setHandlers( new Handler[] { context } ); server.setHandler( handlers );
Пробуем запустить сервер. Для того, чтобы работа программы не прекратилась, ждём завершения процесса сервера главным потоком посредством вызова server.join(). В случае возникновения ошибки печатается соответствующее сообщение.
try { server.start(); System.out.println("Listening port : " + port ); server.join(); } catch (Exception e) { System.out.println("Error."); e.printStackTrace(); }
Исходный код программы можно найти в репозитории GitHub.
Доступ к сервису из браузера
Запуск сервера
При запуске сервера Eclipse может предложить два варианта. Так как сервер содержит полноценный сервлет, то программа может быть запущена на сервере приложений, таком как к примеру Tomcat или самостоятельный Jetty. Однако так как мы встроили jetty в приложение, оно может работать самостоятельно – как Java Application.

После запуска приложение выдаёт соответствующие уведомления и строку Listening port: port, указывающую что наш сервер запущен и ждёт запросов.

Посылка запросов посредством браузера
Наиболее п��остой способ проверить функциональность сервера – обратиться к нему посредством браузера.
При посылке строки запроса, такой как http://localhost:8080/func?a=8.78&b=4.15&op=+ напрямую, сервер выдает ошибку. Дело в том, что строка не соответствует стандарту запросов и должна быть кодирована как URL ( символ + не допустим ).

После кодирования все работает без ошибки. Символ + кодирован URL как %2B, что делает запрос соответствующим стандарту. В интернете имеется множество он-лайн кодировщиков/де-кодировщиков URL, которыми можно воспользоваться для этой цели.
Стандартизированный запрос: http://localhost:8080/func?a=8.78&b=4.15&op=%2B

Аналогичным способом можно проверить ответы сервера на другие запросы.
Клиенты сервера
Использование браузера и прямая посылка запросов непрактичны, так как при ручном формировании строки запроса очень вероятно допущение ошибки. Использование подобного ресурса может быть организовано посредством:
- специализированной веб страницы с автоматическим формированием строки запроса и форматированием ответа посредством JavaScipt;
- мобильным приложением;
- другим сервером, потребляющим созданный ресурс для своих внутренних нужд.
Клиент – веб страница
Специализированная веб страница – простой тип клиентского приложения.

HTML код страницы можно найти в репозитории GitHub.
Создание запускаемого модуля
Созданный сервер можно оформить как единый независимый запускаемый JAR-файл. Такой файл будет требовать только наличия установленной среды выполнения Java и запускаться из любой папки файловой системы. Для создания такого файла кликнем правой кнопкой мыши на наименовании проекта в панели Project Explorer, в контекстном меню выберем Export и далее Export…. В секции Java выберем Runnable JAR file и кликнем кнопку Next.

В настройках создаваемого JAR-файла указываем Launch configuration как Main-SCalculator, полное имя экспортируемого файла и флажок упаковки необходимых модулей в этот файл.

Запуск правильно созданного JAR-файла с именем SCalculator осуществляется простой командой (при запуске из той же папки, где он находится):
java -jar SCalculator.jar

Также возможен запуск сервера двойным кликом мыши на JAR-файле.
Итоги
Многие описанные в этом выпуске элементы были практически использованы при создании высоконагруженных серверов. Несомненно были использованы и более продвинутые приёмы, позволившие достигнуть высокого быстродействия и надёжности, такие как использование сервера NGINX в режиме обратного прокси. Однако все начинается с простого и я надеюсь что смог просто и понятно описать приёмы, которые пригодятся при практической разработке.
Ссылки
Подробнее о встраивании Jetty в приложение можно почитать по ссылке http://docs.codehaus.org/display/JETTY/Embedding+Jetty
Подключение пользовательских библиотек на примере Tomcat описано по ссылке http://www.avajava.com/tutorials/lessons/what-is-a-user-library-and-how-do-i-use-it.html?page=1
Репозиторий GitHub можно найти тут: https://github.com/dgakh/Studies/tree/master/Java/SWS-Embedded-Jetty
Представленный материал основан на использовании Eclipse Luna for Java EE Developers и Ubuntu 14.04.
