Log 2. Spring MVC. Обреченный JSP
Извиняюсь за свое долгое отсутствие. В этом посте мы продолжим разрабатывать наше веб-приложение. Кстати, за время своего отсутствия я обновил свою IntelliJ IDEA до версии 2017.1.3 и вам советую сделать то же.
В прошлом посте нам удалось отобразить в браузере вот такую страницу:
За то, что вы видите на этой странице, отвечает файл index.jsp. Этот файл вы легко найдете, если пробежитесь по дереву директорий нашего проекта, но лучше я вас научу как искать любые файлы в IDEA. Просто быстро нажмите клавишу Shift два раза и в появившемся окне начните вбивать название искомого файла.
Перемещаться по списку можно с помощью клавиш вверх и вниз. После того, как вы найдет необходимый файл, нажмите Enter чтобы перейти к нему.
То, что находится в этом файле называется JSP-шаблон. Шаблонизаторы подобные JSP позволяют нам динамически формировать веб-страницу в ответ на запрос с клиента. Тут стоит заметить, что использование шаблонизаторов - это не единственный способ динамически формировать контент, и вообще они выходят из моды. В нашем приложении мы будем их использовать крайне ограниченно (у нас, как никак, Single Page Application). Основную работу по формированию содержимого веб-страниц наше приложение будет делать с помощью JavaScript и Ajax-запросов.
Ну что ж, пора изменить содержимое нашей единственной страницы.
В файле index.jsp строку
<title>$Title$</title>
Замените на
<title>My WebApp</title>
А строку
$END$
Замените на
Главная страница
Теперь обновим страницу. Возвращаемся в браузер, жмем F5. И… Видим что ничего не изменилось. Как же так?!
Для того чтобы выяснить в чем дело нам нужно проверить какой ответ приходит с сервера. Как это сделать? Да очень просто: нужно нажать Ctrl + Shift + I чтобы открыть панель разработки. Там в вкладке с сетью можно посмотреть как происходит взаимодействие с сервером.
Здесь мы видим, что запрос с url / возвращает HTML-код, который не отражает наших изменений в шаблоне. Это происходит потому, что мы не обновили файлы нашего веб-приложения, которое сейчас крутится на сервере. Но ведь мы внесли изменения в файл index.jsp, почему же они не подтянулись? Дело в том, сервер “сервит” не те файлы, что лежат у нас в исходниках, а те, что находятся артефакте (результате сборки) нашего приложения.
Присмотритесь к структуре каталогов нашего проекта. В папке build, в одной из подпапок находится папка с названием tutorial-1.0-SNAPSHOT.war. Содержимое этой папки и является веб-приложением, которое наш сервер разместил по адресу localhost:8080/.
Как мы видим, содержимое файла index.jsp осталось прежним. Это необходимо исправить. Правда, можно и не исправлять, но в таком случае наше приложение навеки останется не обновленным. Самый простой (и самый тупой) способ - это взять и скопировать файл index.jsp из папки webapp в папку tutorial-1.0-SNAPSHOT.war. Но мы так делать не будем, мы сделаем лучше. Дело в том, что копирование наших исходников из нашего исходного каталога в каталог, за которым следит сервер - это то, что называется deployment (деплоймент). Наша IDE делает это за нас и уже один раз это сделала, ведь каталога tutorial-1.0-SNAPSHOT.war изначально не было. Чтобы разобраться где это происходит еще раз откройте конфигурацию запуска нашего приложения.
Tomcat 8 > Edit Configuration...
В окне конфигурации, в самом низу в разделе Before launch есть команда для IDE, которая называется
Build ‘Gradle: com.brainburns:tutorial-1.0-SNAPSHOT.war(exploded)’ artifact
Именно она из исходников нашего проекта создает папку tutorial-1.0-SNAPSHOT.war (кстати, раз у нас сборка “exploded”, то в результате получается папка, в ином случае результатом сборки был бы архив с таким же названием).
Ну, вроде разобрались. Теперь пора наконец обновить файл index.jsp. В панели дебага щелкните на иконку с синей закольцованной стрелкой (либо просто Ctrl + F10).
В открывшемся окне выберите Redeploy и щелкните на ОК.
Теперь обновив страницу мы можем лицезреть результат наших титанических усилий.
Настало время создать еще одну страницу и подключить Spring к нашему проекту. На этот раз создадим страницу login, на которой пользователи будут (в будущем) регистрироваться и логиниться в наше приложение. Когда страница будет готова, в браузере она будет отображаться вот так:
Здесь видно, что по URL /login у нас должна быть страница с заголовком “Login” и содержимым “Login here”.
Итак, приступаем. Первое, что нужно сделать, это создать JSP-шаблон страницы. Для этого в дереве проекта выберите директорию webapp после чего нажмите Alt + Insert и в контекстном меню выберите JSP/JSPX.
После этого в появившемся модальном окне вбейте название страницы login и щелкните OK.
В созданном файле измените заголовок страницы и ее контент.
Ну вот страница создана. Теперь нам нужно отобразить ее в браузере. Это можно сделать несколькими способами, но так-как это туториал по Spring, мы воспользуемся фреймворком Spring MVC.
Начнем с создания файла-конфигурации. В директории src/main/java создайте новый пакет. Вы ведь не забыли что, чтобы создать что-либо в нужной директории нужно выделить ее и нажать Alt + Insert?
Назовем его com.brainburns.tutorial. В Java пакеты - это структура каталогов. IDEA позволяет легко создавать такую структуру. Таким образом, будет создана структура каталогов com > brianburns > tutorial.
Если вы спросите почему каталоги имеют именно такие имена, то это потому что согласно конвенции название пакета должно соответствовать groupId и artifactId проекта.
В новосозданном каталоге создайте java класс с именем WebConfig
. Этот файл будет нашей Spring-конфигурацией, где мы будет настраивать фреймворк Spring MVC. Для этого наш класс конфигурации должен наследовать класс WebMvcConfigurerAdapter
. Прописываем наследование и… видим как WebMvcConfigurerAdapter подсвечивается красным.
Хм.. Похоже тут ошибка. Кажется мы забыли добавить импорт. Я могу вам подсказать что импорт класса WebMvcConfigurerAdapter должен выглядеть вот так:
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
Вот только даже если вы добавите эту строку перед объявлением класса ошибка никуда не денется. А все потому, что нашему проекту не хватает зависимостей (библиотек). Нашему проекту необходим фреймворк Spring MVC, так давайте добавим его!
Переходим в файл build.gradle и в блок dependencies добавляем строку
compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.8.RELEASE'
Должно получиться вот так:
dependencies {
compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.8.RELEASE'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
Нажимаем на кнопку Refresh all gradle projects (2 зацикленные стрелки во вкладке Gradle справа).
После этого можно заметить как много новых библиотек появилось в нашем проекте:
Теперь возвращаемся к классу WebConfig
. В нем мы видим как IDEA нашла нужный нам класс в загруженных библиотеках и предлагает нам его импортировать.
Нажимаем Alt + Enter и в файле появляется нужная строка импорта. Сейчас класс WebConfig выглядит вот так:
package com.brainburns.tutorial;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* Created by arthan on 21.05.2017. | Project tutorial
*/
public class WebConfig extends WebMvcConfigurerAdapter {
}
Теперь нужно добавить две аннотации к классу: аннотацию @EnableWebMvc
, которая включит поиск классов mvc-контроллеров (о которых пойдет речь позже) и аннотацию @Configuration
, которая пометит наш класс как Spring-конфигурацию и мы сможем объявляем в нем Spring-бины. После этих манипуляций наш класс примет следующий вид:
package com.brainburns.tutorial;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* Created by arthan on 21.05.2017. | Project tutorial
*/
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
}
Хорошо, пустой класс-конфигурация у нас есть. Вот только на наше веб-приложение он сейчас никак не влияет, так как контейнер сервлетов (у нас это сервер Tomcat) никак с этим классом не взаимодействует. Исправим же это!
В том же самом пакете что и класс WebConfig создайте класс WebInitializer
и отнаследуйте его от класса AbstractAnnotationConfigDispatcherServletInitializer
(до чего же элегантные в Spring названия классов)
IDEA сразу же подчеркнула объявление класса красным, потому что мы наследуемся от абстрактного класса, но не имплементируем абстрактные методы. Чтобы это поправить переместите курсор внутрь тела класса и нажмите Ctrl + I. В открывшемся окне все нужные методы уже выбраны, поэтому просто нажимаем ОК.
Как вы можете заметить, IDEA создала дефолтные реализации для трех методов. Однако нам нужно чтобы методы getRootConfigClasses
и метод getServletConfigClasses
возвращали наш класс-конфигурацию (завернутый в массив, раз методы так требуют). Ну а метод getServletMappings
должен возвращать строку “/” (тоже завернутую в массив). Соответствующим образом изменяем реализацию этих методов:
package com.brainburns.tutorial;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* Created by arthan on 21.05.2017. | Project tutorial
*/
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] {WebConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {WebConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[] {"/"};
}
}
Вам наверно интересно что вообще делает класс WebInitializer. Здесь происходит конфигурация главного entry-point’a веб-приложения на Spring - сервлета DispatcherServlet
. WebInitializer через цепочку наследования имплементирует интерфейс WebApplicationInitializer
. А за этим интерфейсом, в свою очередь, “следит” уже внутренний спринговый класс SpringServletContainerInitializer
, который обязательно запустится при старте контейнера сервлетов. В методах getRootConfigClasses
и getServletConfigClasses
мы возвращаем наш класс конфигурации чтобы при старте сервера создавался необходимый нам спринговый контекст (который сейчас пуст).
Что ж, теперь наш WebConfig работает, а значит самое время создать контроллер. В том же пакете создайте класс PageController
. Примените к нему аннотацию @Controller
и создайте внутри него метод getLoginPage
:
package com.brainburns.tutorial;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Created by arthan on 21.05.2017. | Project tutorial
*/
@Controller
public class PageController {
@GetMapping("/login")
public String getLoginPage() {
return "login";
}
}
Так, разберем по частям нами написанное. Аннотация @Controller
делает наш класс MVC-контроллером. Это значит что методы нашего класса будут принимать HTTP запросы и обрабатывать их. Вот и метод getLoginPage
с аннотацией @GetMapping
должен будет принять запрос с URL /login и обработать его. В данном случае он возвращает название отображения (view). Когда в этот метод придет запрос и вернется “login”, Spring MVC попытается найти подходящее и с его помощью составить страницу в ответ на пришедший запрос. Мы сейчас хотим чтобы запрос был переведен на login.jsp. И для этого мы должны сделать следующее.
В классе WebConfig нужно объявить бин с типом InternalResourceViewResolver
. Делается это следующим образом:
package com.brainburns.tutorial;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
/**
* Created by arthan on 21.05.2017. | Project tutorial
*/
@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setSuffix(".jsp");
viewResolver.setViewClass(JstlView.class);
return viewResolver;
}
}
Здесь мы создаем метод viewResolver
, создающий и настраивающий экземпляр класса InternalResourceViewResolver
. Этот класс с говорящим именем позволяет настроить то, как будет искаться представление, имя которого возвратит метод в контроллере.
Метод setSuffix
устанавливает суффикс для поиска, здесь он “.jsp” а это значит что раз мы возвращаем строку “login” фреймворк будет искать файл “login.jsp”. Ну а метод setViewClass
, говорит что искать мы будем именно JSP-файл.
На этом настройка приложения завершена, и теперь можно его запустить, чтобы убедиться что все работает. Запустим наше приложение в дебаг режиме с помощью кнопки или сочетанием Shift + F9.
Запускаем.
Кажется что-то пошло не так:
Ошибка компиляции! И это при том что все наши файлы корректны. В чем же дело? Тут гадать не надо, ибо сообщение об ошибке однозначно заявляет об отсутствии класса javax.servlet.ServletException
. Вот только причем здесь этот класс, если мы его даже нигде не импортируем? А дело в том, что этот класс нужен для работы Spring MVC. Можете поискать у нас в зависимостях класс AbstractDispatcherServletInitializer
, который использует этот отсутствующий класс. Так вот, приложение даже не компилируется, как же нам исправить ситуацию? Да очень просто - нужно добавить еще одну зависимость. На этот раз в блоке dependencies в файле build.gradle добавьте строку:
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
В добавленной библиотеке находятся необходимые нам классы. Вам наверное интересно почему на этот раз строка начинается с compileOnly а не с compile. Тут дело в том, что эту библиотеку мы используем только для компиляции, в артефакт она не попадет. Это потому что, на сервере Tomcat она уже присутствует.
Попробуем запустить наше приложение еще раз (предварительно обновив gradle зависимости, разумеется). Похоже что на этот раз все собралось. В браузере переходим по URL http://localhost:8080/login
и видим следующую картину:
Знаменитая ошибка 404 явила себя нашему взору. Где же мы прокололись на этот раз? Возвращенный http код 404 говорит о том, что URL /login ничто не обрабатывает. Но ведь мы создавали контроллер, почему же запрос до него не дошел? Дело в том, что мы не связали наш класс-контроллер со Spring MVC. Фреймворк просто не знает о нашем контроллере. Исправим это!
Для этого нам нужно добавить к классу WebConfig вот такую аннотацию:
@ComponentScan(basePackages = "com.brainburns.tutorial")
Благодаря этой аннотации Spring будет знать в каком пакете искать контроллеры. Пробуем запуститься еще раз. И...
Похоже, наше приложение отказывается работать! Ну ничего, еще рано опускать руки.
Внимательно присмотревшись к ошибке можно заметить, что нам не хватает класса javax/servlet/jsp/jstl/core/Config
. Он нужен для того чтобы работали JSP шаблоны. Добавим еще одну строчку к списку зависимостей:
dependencies {
compile group: 'org.springframework', name: 'spring-webmvc', version: '4.3.8.RELEASE'
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
compile group: 'javax.servlet', name: 'jstl', version : '1.2'
testCompile group: 'junit', name: 'junit', version: '4.12'
}
Помолясь (и обновив gradle), запускаем приложение в 4-й раз.
Ура, получилось! Столько мороки и ради чего? На самом деле, сегодня мы заложили каркас нашего приложения, что позволит в будущем быстрее разрабатывать и настраивать его.
Вот и подошел к концу второй пост посвященный построению веб-приложения на Spring MVC. Не забудьте закомитить все изменения и назовите коммит “Spring MVC is Added!”
Еще раз напоминаю что скачать разрабатываемое приложение можно отсюда:
https://github.com/arthan1011/brainburns-tutorial
Если что-то непонятно или если вы столкнулись какими-либо сложностями, задавайте вопросы. Постараюсь ответить на все.
Keep calm and keep coding.