Статьи

Гранд Тур: Kickin ‘It MVC Стиль

Январь оказывается довольно легким месяцем для новостей Java. Ну что ж, у меня будет больше времени, чтобы продолжить мой грандиозный тур по веб-разработке на Java! Все еще следуете?

В прошлый раз я показал вам, как преобразовать сервлет «список дел» в JSP. К сожалению, то, что мы получили, было не намного лучше с точки зрения читабельного кода — код Java, встроенный в документ HTML, не намного более читабелен, чем код HTML, встроенный в класс Java.

Что делать? Ну, пока у нас есть два инструмента: сервлеты и JSP. Сервлеты отлично подходят для содержания кода Java, а JSP — для размещения кода HTML. Ответ состоит в том, чтобы разделить наше приложение так, чтобы код Java входил в сервлеты, а код HTML — в JSP.

Стандартный подход для этой модели под названием Model 2 Architecture существует уже давно, но в последнее время он стал известен под более наглядным названием: Model-View-Controller (MVC).

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

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

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

Модель, как правило, лучше всего начинать, так как вы можете написать ее, не сильно заботясь о веб-интерфейсе. В нашем приложении списка дел у нас уже есть модель: она состоит из классов ToDoList и ToDoItem . Эти два класса выполняют всю работу по управлению списком дел.

Пока мы смотрим на эти классы, давайте сделаем одну небольшую настройку метода getToDoItems : мы сделаем так, чтобы он возвращал List а не Iterator . Это дает немного больше гибкости в том, как элементы списка дел могут использоваться при их получении с помощью этого метода.

  public List getToDoItems() { refreshList(); return (List)list.clone(); } 

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

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

Мы начнем с ToDoServlet , который будет обрабатывать запросы на просмотр списка дел. Первое, что нужно сделать сервлету, это настроить модель — экземпляр ToDoList . Когда мы впервые посмотрели на сервлеты , мы сделали это с помощью метода сервлета init() , который хранил ToDoList в переменной экземпляра сервлета.

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

Для выполнения задач инициализации для всего приложения мы должны использовать ServletContextListener . Вместо того, чтобы расширять класс, это интерфейс, который может реализовать любой класс. Так как наш «основной» сервлет является хорошим местом для этого, мы сделаем так, чтобы он реализовал этот интерфейс:

 public class ToDoServlet extends HttpServlet implements ServletContextListener { 

Когда класс реализует ServletContextListener , он должен содержать два метода: contextInitialized , который выполняет задачи инициализации приложения , и contextDestroyed , который выполняет очистку приложения:

  public void contextInitialized(ServletContextEvent sce) { ServletContext sc = sce.getServletContext(); sc.setAttribute("toDoList", new ToDoList(sc.getInitParameter("jdbcDriver"), sc.getInitParameter("jdbcConnectionString"))); } public void contextDestroyed(ServletContextEvent sce) { } 

Все действия здесь находятся в contextInitialized , так как нам не нужно делать ничего особенного, когда приложение закрывается. Как вы можете видеть, оба этих метода получают ServletContextEvent при вызове, из которого мы можем получить ссылку на ServletContext . ServletContext — это объект, который представляет веб-приложение в целом — это контекст, в котором работает сервлет . Все сервлеты и JSP имеют доступ к ServletContext ; следовательно, это идеальное место для хранения нашей модели!

ServletContext позволяет вам устанавливать и получать атрибуты, которые являются просто значениями всего приложения, связанными с строковыми метками. Мы будем хранить нашу модель, новый объект ToDoList , в атрибуте с меткой "toDoList" .

Прежде чем мы продолжим, внимательно рассмотрим код, который создает ToDoList :

  new ToDoList(sc.getInitParameter("jdbcDriver"), sc.getInitParameter("jdbcConnectionString"))); 

Как мы делали ранее, мы получаем значения драйвера JDBC и строки подключения, требуемые этим классом, из параметров инициализации, но поскольку мы выполняем инициализацию для всего нашего приложения, параметры инициализации для ToDoServlet еще не доступны! Вот почему мы вызываем метод getInitParameter объекта ServletContext , а не сам сервлет, как мы привыкли делать. Они известны как параметры инициализации контекста , в отличие от параметров инициализации сервлета.

Итак, у нас есть ServletContextListener который готов инициализировать наше приложение, если оно получает необходимые параметры инициализации контекста . Как вы уже догадались, нам нужно добавить некоторые вещи в наш 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>ToDoMVC</display-name> <listener> <listener-class>com.sitepoint.ToDoServlet</listener-class> </listener> <context-param> <description>The JDBC driver class.</description> <param-name>jdbcDriver</param-name> <param-value>com.mysql.jdbc.Driver</param-value> </context-param> <context-param> <description>The JDBC connection string.</description> <param-name>jdbcConnectionString</param-name> <param-value>jdbc:mysql://localhost/todo?user=root&amp;password=password</param-value> </context-param> </web-app> 

Тег <listener> позволяет приложению узнать о предоставленном нами слушателе контекста, а теги <context-param> задают наши параметры инициализации контекста всего приложения.

Теперь, прежде чем мы увлеклись всем этим процессом инициализации приложения, я помню, что мы писали сервлет. Итак, давайте посмотрим на методы doGet и doPost :

  public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ToDoList toDoList = (ToDoList)getServletContext().getAttribute("toDoList"); List toDoItems = toDoList.getToDoItems(); request.setAttribute("toDoItems", toDoItems); RequestDispatcher view = request.getRequestDispatcher("/todo.jsp"); view.forward(request, response); } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } 

Посмотри на это! Сервлет без встроенного HTML-кода! Как такое возможно? Хорошо помните, этот сервлет является просто контроллером в нашем дизайне MVC — после того, как все сделано с моделью, он передает запрос представлению для отображения реальной веб-страницы. Присмотревшись к методу doGet , вы поймете, что именно это и делает:

  ToDoList toDoList = (ToDoList)getServletContext().getAttribute("toDoList"); List toDoItems = toDoList.getToDoItems(); request.setAttribute("toDoItems", toDoItems); 

Здесь он выбирает модель (список ToDoList сохраненный в атрибуте контекста сервлета), использует его для получения списка элементов списка дел, а затем сохраняет этот List где-нибудь, где представление сможет его найти, а именно в объект request . Оказывается, объект request поддерживает атрибуты, и это идеальный способ передачи значений в представление, которое потребуется для построения страницы.

После тяжелой работы мы готовы передать запрос представлению (в этом случае JSP с именем todo.jsp в корне нашего приложения):

  RequestDispatcher view = request.getRequestDispatcher("/todo.jsp"); view.forward(request, response); 

На первый взгляд это может показаться немного запутанным, но эту формулу мы всегда будем использовать для пересылки запросов из сервлета в JSP. Мы получаем объект RequestDispatcher для местоположения, которое мы хотим переслать (в этом случае "/todo.jsp" указывает файл todo.jsp в корне приложения), затем мы вызываем его метод forward , передавая ему request и Объекты response , необходимые для выполнения работы.

Итак, мне пришлось немного постучать, чтобы объяснить все новое, что происходит в этом сервлете, но если вы отступите и посмотрите на код, вы обнаружите, что он на самом деле довольно аккуратный и аккуратный. А так как они не обременены всей программой установки приложений, два наших других сервлета еще аккуратнее и опрятнее!

 package com.sitepoint; import javax.servlet.*; import javax.servlet.http.*; import java.io.*; public class AddToDoServlet extends HttpServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String newToDo = request.getParameter("newtodo"); if (newToDo != null) { ToDoList toDoList = (ToDoList)getServletContext().getAttribute("toDoList"); toDoList.addItem(newToDo); } response.sendRedirect("index.html"); } } 
 package com.sitepoint; import javax.servlet.*; import javax.servlet.http.*; import java.io.*; public class DeleteToDoServlet extends HttpServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String deleteid = request.getParameter("deleteid"); if (deleteid != null) { try { ToDoList toDoList = (ToDoList)getServletContext().getAttribute("toDoList"); toDoList.deleteItem(Integer.parseInt(deleteid)); } catch (NumberFormatException e) { throw new ServletException("Bad deleteid value submitted.", e); } } response.sendRedirect("index.html"); } } 

Вы можете быть удивлены, заметив, что эти два сервлета на самом деле не перенаправляют в JSP после выполнения своей работы. Вместо этого они перенаправляют браузер в "index.html" , который мы вскоре настроим так, чтобы он указывал на наш основной ToDoServlet .

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

Чтобы отшлифовать наши сервлеты, нам нужно назначить URL для каждого из них в нашем файле web.xml . Мы будем использовать /index.html для ToDoServlet , так как мы хотим, чтобы он появлялся по умолчанию, когда пользователь загружает каталог нашего приложения. AddToDoServlet и DeleteToDoServlet получат /AddItem.do и /DeleteItem.do соответственно, в соответствии с соглашением о предоставлении «action» URL-адресов .do расширениям в веб-приложениях Java.

  <servlet> <servlet-name>todoservlet</servlet-name> <servlet-class>com.sitepoint.ToDoServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>todoservlet</servlet-name> <url-pattern>/index.html</url-pattern> </servlet-mapping> <servlet> <servlet-name>addtodoservlet</servlet-name> <servlet-class>com.sitepoint.AddToDoServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>addtodoservlet</servlet-name> <url-pattern>/AddItem.do</url-pattern> </servlet-mapping> <servlet> <servlet-name>deletetodoservlet</servlet-name> <servlet-class>com.sitepoint.DeleteToDoServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>deletetodoservlet</servlet-name> <url-pattern>/DeleteItem.do</url-pattern> </servlet-mapping> 

После того, как наши сервлеты готовы, осталось только создать представление для нашего приложения. Для этого примера нам нужен только один файл JSP: todo.jsp .

Теперь, когда мы вытащили весь наш код инициализации и обработки запросов в сервлеты, наш JSP-файл выглядит намного более разумным:

 <%@ page import="java.util.*,com.sitepoint.*,uk.co.anyware.html.*" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>To-Do List</title> <meta http-equiv="content-type" content="text/html; charset=iso-8859-1" /> <link rel="stylesheet" type="text/css" href="<%= request.getContextPath() %>/styles.css" /> </head> <body> <% List toDoItems = (List)request.getAttribute("toDoItems"); Iterator it = toDoItems.iterator(); if (it.hasNext()) { %> <form action="DeleteItem.do" method="post"> <select name="deleteid" size="<%= Math.max(2, toDoItems.size()) %>"> <% while (it.hasNext()) { ToDoItem toDoItem = (ToDoItem) it.next(); %> <option value="<%= toDoItem.getId() %>"><%= HTMLEscaper.escape(toDoItem.toString()) %></option> <% } %> </select> <input type="submit" value="Delete Selected Item" /> </form> <% } %> <form action="AddItem.do" method="post"> <input type="text" name="newtodo" /> <input type="submit" value="Add New Item" /> </form> </body> </html> 

Если вы поняли JSP, который мы создали в прошлый раз , то это должно быть просто! Код скриптлета просто извлекает List из атрибута request "toDoItems" чтобы создать список элементов списка дел, и наши две формы теперь отправляют URL-адреса .do мы присвоили соответствующим сервлетам. Помимо этих изменений, это тот же старый JSP с гораздо меньшим количеством Java-кода.

И вот оно у нас: веб-приложение MVC Java! Вот структура каталогов:

  /todo.jsp
 /styles.css
 /WEB-INF/web.xml
 /WEB-INF/classes/com/sitepoint/AddToDoServlet.class
 /WEB-INF/classes/com/sitepoint/AddToDoServlet.java
 /WEB-INF/classes/com/sitepoint/DeleteToDoServlet.class
 /WEB-INF/classes/com/sitepoint/DeleteToDoServlet.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-version-bin.jar 

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

Это приложение начинает выглядеть чертовски хорошо, даже если я сам так говорю. Некоторый код может быть немного труден для ввода вручную, но с хорошей Java IDE для написания стандартного кода для вас (с большой новой версией, выпущенной только в прошлом месяце, NetBeans является популярным бесплатным выбором), много от рутины уходит.

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