Развитие информационных технологий все более и более вовлекает использование инфраструктуры Интернет. Распределенные и мобильные приложения все чаще используют обмен информацией по протоколу 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.
dborovikov
Вы не используете сисему управления зависимостями, у вас хромает code style. Саму службу было бы проще сделать с помощью Jersey, а не вручную. Там в итоге одна страница кода была бы + файл с зависмостями (pom.xml, gradle.build, etc), а вы тут что-то понаписали. Вобщем, вам есть куда разбираться самим =)
dgakh Автор
Эта статья имеет свою цель. Если бы я планировал показать возможности и мощь инструментария Java, я бы и пример посложнее привёл. К тому же я упомянул в статье что расчитываю на начинающих программистов и не хотел вводить новые понятия. Правда я вёл рассчет на начинающих из моего круга знакомых, кто работал с Delphi, C#, не в веб, а не чисто с 0.
С другой стороны у меня зреет еще одна статья, которая будет ссылаться на данную и рассчитана именно на представленную тут структуру и текста и кода.
В любом случае спасибо — может запланирую еще одну статью как эту же службу реализовать по другому и сравнение разницы тоже будет интересно.
bromzh
Для начинающих как раз полезно было бы объяснить, как подключать Jersey. Тем более, что это куда проще, быстрее и надёжнее.