Статьи

Гранд Тур: Сервлеты

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

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

Основные запросы страниц обрабатываются методом doGet() . Отправки форм, использующие метод POST, обрабатываются методом doPost() . Объекты, передаваемые этим методам, обеспечивают доступ к информации о запросе браузера и позволяют контролировать ответ сервлета.

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

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

Итак, начнем с того, что поместили уже имеющиеся вещи в правильную структуру каталогов. Создайте новый пустой каталог для работы, затем создайте подкаталог с именем WEB-INF . Все «обычные» веб-ресурсы (HTML-страницы, изображения, таблицы стилей и файлы JavaScript) будут находиться в главном каталоге, тогда как все компоненты Java (классы, библиотеки и файлы конфигурации) будут находиться в WEB-INF .

В WEB-INF создайте еще два подкаталога: classes и lib . WEB-INF/classes будут содержать все классы Java для нашего веб-приложения, а классы WEB-INF/lib будут содержать любые библиотеки, которые могут потребоваться этим классам.

Говоря о необходимых библиотеках, помните, что в прошлый раз нашему классу ToDoList требовался драйвер JDBC для доступа MySQL (MySQL Connector / J) к базе данных. Для автономного приложения нам нужно было добавить файл JAR ( mysql-connector-java- version -bin.jar ) в mysql-connector-java- version -bin.jar к классам. Для веб-приложения Java просто перетащите файл в каталог WEB-INF/lib .

Поскольку мы будем использовать классы, которые мы уже разработали ( ToDoList и ToDoItem ), в нашем веб-приложении, мы должны поместить их в каталог WEB-INF/classes . Вы могли бы также поместить туда исходные файлы вместе со скомпилированными классами, на тот случай, если вам нужно будет внести какие-либо изменения — тогда перекомпиляция будет проще простого.

Итак, вот наша структура файлов и каталогов:

  /WEB-INF/classes/com/sitepoint/ToDoItem.class
 /WEB-INF/classes/com/sitepoint/ToDoItem.java
 /WEB-INF/classes/com/sitepoint/ToDoList.class
 /WEB-INF/classes/com/sitepoint/ToDoList.java
 / WEB-INF / lib / mysql-connector-java- версия -bin.jar 

Давайте теперь обратим наше внимание на создание сервлета для нашего приложения. Помните, что сервлеты — это просто классы Java, и этот будет com.sitepoint.ToDoServlet :

 package com.sitepoint; import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class ToDoServlet extends HttpServlet { 

Как видите, классы сервлетов должны расширять класс javax.servlet.HttpServlet . Вы можете просмотреть документацию по этому и смежным классам в Спецификации J2EE API ( также в JDocs ).

Наш сервлет будет использовать экземпляр нашего класса ToDoList , который мы хотим создать при первой загрузке сервлета. Мы можем выполнить такие задачи инициализации в методе init() , который будет вызывать сервер при загрузке сервлета:

  private ToDoList toDoList; // Initialize global variables public void init() throws ServletException { toDoList = new ToDoList(getInitParameter("jdbcDriver"), getInitParameter("jdbcConnectionString")); } 

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

Вы заметите, что метод init() , как и все стандартные методы сервлета, может генерировать ServletException . Это исключение используется, чтобы сервер знал, что в сервлете что-то пошло не так. Посмотрим, как это работает чуть позже.

Далее следует метод doGet() , который обрабатывает обычные запросы браузера. Он принимает два параметра: HttpServletRequest который содержит подробную информацию о запросе браузера, и HttpServletResponse , который сервлет может использовать для управления своим ответом браузеру:

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

Еще раз, метод может ServletException чтобы сообщить об ошибке в сервлете. Спецификация сервлета также позволяет этой функции генерировать IOException в случае, если есть проблема с чтением запроса или записью ответа.

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

  response.setContentType("text/html"); 

Теперь мы можем запросить у него объект PrintWriter который можно использовать для отправки HTML-кода в браузер:

  PrintWriter out = response.getWriter(); 

Объект PrintWriter позволяет нам отправлять HTML-код в браузер, используя его метод println() :

  out.println("<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"n" + " "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">"); out.println("<html xmlns="http://www.w3.org/1999/xhtml">"); out.println("<head>"); out.println("<title>To-Do List</title>"); out.println("<meta http-equiv="content-type" " + "content="text/html; charset=iso-8859-1" />"); 

Если вам это не очень нравится, вы заметили самую большую слабость сервлетов: разметка HTML смешивается с логикой Java вашего приложения, что затрудняет чтение и поддержку.

Нажав на, мы хотим связать таблицу стилей CSS с нашей страницей:

  out.println("<link rel="stylesheet" type="text/css" href="" + request.getContextPath() + "/styles.css" />"); out.println("</head>"); out.println("<body>"); 

Поскольку мы еще не знаем, в какой каталог веб-сервера будет установлено наше приложение, мы не знаем пути, где будут доступны такие ресурсы, как таблицы стилей и изображения. Приведенный выше код использует метод объекта request getContextPath() для получения пути к корню нашего приложения, и оттуда мы можем указать на файл styles.css , который мы создадим чуть позже.

Далее мы покажем наш список дел в меню формы HTML. Мы можем начать с захвата Iterator из нашего ToDoList с getToDoItems() метода getToDoItems() , а затем проверить, содержит ли он какие-либо элементы с hasNext() метода hasNext() :

  Iterator toDoItems = toDoList.getToDoItems(); if (toDoItems.hasNext()) { 

Мы хотим, чтобы форма отправлялась обратно по тому же URL-адресу, чтобы этот сервлет мог также обрабатывать этот запрос, поэтому мы используем метод getRequestURI() для его получения:

  out.println("<form action="" + request.getRequestURI() + "" method="post">"); 

Поскольку меню HTML-форм превращаются в выпадающие списки size 1, мы хотим, чтобы <select> имел size как минимум 2, но мы также увеличиваем его, чтобы вместить количество элементов в наших делах. список. Мы используем метод Math.max() выбирающий между 2 и общим количеством элементов в списке, как указано в getItemCount() нашего ToDoList :

  out.println("<select name="deleteid" size="" + Math.max(2, toDoList.getItemCount()) + "">"); 

Затем мы можем toDoItems с помощью цикла while. Каждый раз в цикле мы ToDoItem из Iterator с помощью next() . Поскольку он не знает, что содержит ToDoItem , нам нужно привести их к нужному классу, прежде чем мы сможем их использовать:

  while (toDoItems.hasNext()) { ToDoItem toDoItem = (ToDoItem) toDoItems.next();

 while (toDoItems.hasNext()) { ToDoItem toDoItem = (ToDoItem) toDoItems.next(); 

Для каждого элемента мы хотим создать <option> с идентификатором элемента в качестве value . Вывод идентификатора не проблема, так как это просто целое число, но сам элемент задачи становится сложным. Что, если элемент списка дел содержит HTML-код, возможно, даже вредоносный код скрипта? Мы не хотим выводить этот материал и интерпретировать его браузером как часть нашего сайта! Решением является экранирование любых специальных символов в значении перед его печатью.

Это еще одна область, где слабые сервлеты: в API сервлетов или в Java вообще нет встроенной функции экранирования HTML (по крайней мере, ни одна из них нам не доступна). К счастью, это давняя проблема, и сторонние классы были созданы, чтобы сделать работу. Тот, который я выбрал, разработан и распространен AnyWare . Класс называется uk.co.anyware.html.HTMLEscaper , а его исходный код и лицензия включены в архив кода, который я предоставил ниже.

Чтобы использовать HTMLEscaper , HTMLEscaper класс (и исходный код, если вы хотите его сохранить) в каталог WEB-INF/classes и добавьте необходимый import в ToDoServlet класса ToDoServlet :

 import uk.co.anyware.html.*; 

Имея класс в руках, мы теперь можем безопасно вывести элемент списка дел. Мы также удалим оставшуюся часть формы с помощью кнопки отправки для удаления выбранных элементов:

  out.println("</select>"); out.println("<input type="submit" value="Delete Selected Item" />"); out.println("</form>"); } 

Мы завершим страницу второй формой, на этот раз для отправки новых задач:

  out.println("<form action="" + request.getRequestURI() + "" method="post">"); out.println("<input type="text" name="newtodo" />"); out.println("<input type="submit" value="Add New Item" />"); out.println("</form>"); out.println("</body>"); out.println("</html>"); out.close(); } 

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

Обе эти формы отправляются с использованием метода POST ( method="post" ), поэтому мы можем обработать их в методе doPost() сервлета. Но чтобы сделать сервлет как можно более гибким, мы также будем поддерживать отправку для добавления новых задач и удаления существующих с помощью запросов GET. Для этого мы просто передадим POST-запросы обратно в наш метод doGet() :

  public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } 

В верхней части doGet() нам теперь нужно проверить и обработать отправленные нами формы. Вот код для обработки новых задач:

  String newToDo = request.getParameter("newtodo"); if (newToDo != null) { toDoList.addItem(newToDo); // Redirect to self response.sendRedirect(request.getRequestURI()); return; } 

Метод getParameter() позволяет нам получить переданное значение с заданным именем в виде строки. Если значение не равно null , мы знаем, что у нас есть представление. Мы добавляем его в список дел с addItem метода addItem , а затем перенаправляем браузер на текущую страницу с помощью метода sendRedirect() ответа.

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

Код для обработки удаленных элементов очень похож, за исключением того, что он должен преобразовать переданную строку в целое число для метода deleteItem() списка deleteItem() :

  String deleteid = request.getParameter("deleteid"); if (deleteid != null) { try { toDoList.deleteItem(Integer.parseInt(deleteid)); // Redirect to self response.sendRedirect(request.getRequestURI()); return; } catch (NumberFormatException e) { throw new ServletException("Bad deleteid value submitted.", e); } } 

Преобразование достигается с помощью Integer.parseInt() , но если переданное значение не может быть преобразовано в число, будет NumberFormatException . Если это происходит, наш код перехватывает исключение и выдает исключение ServletException (которому мы передаем NumberFormatException в качестве основной причины).

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

Хотите верьте, хотите нет, но это все для сервлета! Вы все еще должны скомпилировать его, и это немного сложно. Видите ли, класс HttpServlet который расширяет наш сервлет, включен в библиотеки, распространяемые с каждым сервером Java, и вам нужен этот класс, чтобы иметь возможность компилировать ваш подкласс.

В Tomcat 5.0 библиотека сервлетов называется servlet-api.jar и ее можно найти в каталоге commonlib вашей установки Tomcat. Поэтому вы можете скомпилировать ваш сервлет, включив этот JAR-файл в classpath:

  javac -classpath
 ".; c: Program FilesApache GroupTomcat 5.0commonlibservlet-api.jar" 
 com / sitepoint / *. java, великобритания / co / anyware / html / *. java 

Вот как должна выглядеть ваша структура файлов и каталогов:

  /WEB-INF/classes/com/sitepoint/ToDoItem.class
 /WEB-INF/classes/com/sitepoint/ToDoItem.java
 /WEB-INF/classes/com/sitepoint/ToDoList.class
 /WEB-INF/classes/com/sitepoint/ToDoList.java
 /WEB-INF/classes/com/sitepoint/ToDoServlet.class
 /WEB-INF/classes/com/sitepoint/ToDoServlet.java
 /WEB-INF/classes/uk/co/anyware/html/HTMLEscaper.class
 /WEB-INF/classes/uk/co/anyware/html/HTMLEscaper.java
 / WEB-INF / lib / mysql-connector-java- версия -bin.jar 

Нам осталось добавить две вещи, чтобы завершить наше веб-приложение на Java. Первая — это styles.css стилей styles.css на styles.css ссылается наш сервлет:

 body, p, td, th { background: #fff; color: #000; font: medium Verdana, Arial, Helvetica, sans-serif; } select { width: 100%; } 

Перетащите это в основной каталог приложения, который также содержит каталог WEB-INF .

Другая вещь, которая нам нужна, это файл конфигурации XML для приложения, web.xml . Этот файл называется дескриптором развертывания . Должно начинаться с пометить как этот:

 <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> 

Далее мы предоставляем имя для нашего веб-приложения:

  <display-name>ToDoServlet</display-name> 

Теперь мы настроим наш сервлет. Мы даем ему имя и указываем его класс:

  <servlet> <servlet-name>toDo</servlet-name> <servlet-class>com.sitepoint.ToDoServlet</servlet-class> 

Теперь запомните, что метод init() нашего сервлета использовал два параметра инициализации: один для имени класса драйвера JDBC и один для строки соединения JDBC. Мы предоставляем значения для этих параметров здесь:

  <init-param> <description>The JDBC driver class.</description> <param-name>jdbcDriver</param-name> <param-value>com.mysql.jdbc.Driver</param-value> </init-param> <init-param> <description>The JDBC connection string.</description> <param-name>jdbcConnectionString</param-name> <param-value>jdbc:mysql://localhost/todo?user=root&password=password</param-value> </init-param> </servlet> 

Наконец, мы предоставляем отображение сервлета, которое отправляет запросы на заданный URL сервлету. В нашем примере мы направим запросы на index.html в корне нашего приложения нашему сервлету:

  <servlet-mapping> <servlet-name>toDo</servlet-name> <url-pattern>/index.html</url-pattern> </servlet-mapping> </web-app> 

Вот и все для нашего приложения! Вот полный список файлов:

  /styles.css
 /WEB-INF/web.xml
 /WEB-INF/classes/com/sitepoint/ToDoItem.class
 /WEB-INF/classes/com/sitepoint/ToDoItem.java
 /WEB-INF/classes/com/sitepoint/ToDoList.class
 /WEB-INF/classes/com/sitepoint/ToDoList.java
 /WEB-INF/classes/com/sitepoint/ToDoServlet.class
 /WEB-INF/classes/com/sitepoint/ToDoServlet.java
 /WEB-INF/classes/uk/co/anyware/html/HTMLEscaper.class
 /WEB-INF/classes/uk/co/anyware/html/HTMLEscaper.java
 / WEB-INF / lib / mysql-connector-java- версия -bin.jar 

Теперь вы готовы объединить это приложение, чтобы оно могло быть развернуто на сервере с поддержкой Java. Для этого вам нужно использовать утилиту jar входящую в комплект JDK. Если вы добавили каталог bin вашего JDK в системный путь, вы сможете запустить его из командной строки, просто набрав jar .

Перейдите в рабочий каталог приложения (тот, который содержит styles.css и WEB-INF ) и введите styles.css команду:

  jar cvf ToDoServlet.war. 

Это создаст файл с именем ToDoServlet.war содержащий ваше приложение. На самом деле это замаскированный ZIP-файл.

Теперь вам нужно развернуть это приложение на вашем сервере. Различные серверы предоставляют разные способы сделать это. Если вы используете Tomcat, просто скопируйте файл webapps каталог webapps вашей установки. Через несколько секунд Tomcat автоматически извлечет файл в новый каталог. Затем вы можете перейти и отредактировать web.xml чтобы убедиться, что строка подключения JDBC содержит правильные данные для вашего сервера базы данных.

Как только это будет сделано, вы должны быть готовы к запуску приложения. Загрузите http://localhost:8080/ToDoServlet/index.html в вашем браузере (при условии, что Tomcat работает на порте 8080 на вашем локальном компьютере). Вот что вы должны увидеть:

Поиграй со страницей. Это должно работать так, как вы ожидаете. Вот файл WAR (полный с исходным кодом, если вы распакуете его):

Загрузить код (250 КБ)