Spring Framework на данный момент времени является, наверное, самым популярным фреймворком в контексте Java разработки. Сегодня мы с вами рассмотрим один из модулей данного фреймворка, а именно - Spring MVC. Мотивацией к написанию этой статьи послужила эта работа и вам лучше сначала ознакомиться с ней, потому что часть кода мы позаимствуем оттуда. Будем сегодня использовать такой же подход, как и в представленной статье, то есть мы будем копировать структуру пакетов и названия классов оригинального спринга.
По окончанию статьи мы реализуем часть функционала
аннотации RequestMapping
, а именно, указание паттеронов и методов,
которые должен обрабатывать аннотированный метод. Для того, чтобы
сильно не раздувать статью, результат работы метода мы сразу же будем
возвращать клиенту, то есть наши контроллеры будут "рестовыми" (RestController
).
В итоге мы получим приложение, которое при запросе ресурса /root/greeting
будет просто возвращать строку с приветствием пользователя. Имя пользователя
будет передаваться в качестве параметра запроса, которое мы получим
с помощью аннотации RequestParam
.
Spring MVC построен на основе Servlet API, поэтому нам нужен контейнер, который реализует данную спецификацию. В моем случае это будет Tomcat. Создайте обычный Maven проект и откройте его в любимой IDE. Теперь чтобы получить доступ к Servlet API во время компиляции нам нужно подключить его в качестве зависимости:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
Это единственная зависимость, которая нам сегодня понадобится. Если в процессе статьи у вас возникнут какие-то проблемы, полный код вы всегда сможете найти в этом репозитоии.
Я думаю, что любой, кто когда-либо работал со Spring MVC, видел картинку, подобную этой:
Spring MVC построен на основе паттерна Front Controller. DispatcherServlet
обрабатывает
любой запрос приходящий на сервер. Далее объект класса HandlerMapping
на основе
информации из аннотаций RequestMapping
находит метод, удовлетворяющий текущему
запросу. Потом найденный метод передается объекту HandlerAdapter
, который
сначала с помощью объектов класса HandlerMethodArgumentResolver
определяет необходимые аргументы для метода, а затем вызывает его
с найденными аргументами. Если метод не аннотирован ResponseBody
, то чаще
всего он возвращает строку, которая является именем шаблона. Вообще со всеми
объектами, которые может возвращать пользовательский метод можно ознакомиться
здесь.
Наконец ViewResolver
определяет нужный шаблон, и он отправляется клиенту.
В прошлой части уже была написана BeanFactory
, однако в той реализации использовался
системный ClassLoader
. Но в нашем случае данный загрузчик будет использоваться
для того, чтобы загрузить классы для работы Tomcat. Вот, посмотрите на иерархию загрузчиков
в веб-приложении:
Как видите у каждого приложения в контейнере есть свой собственный загрузчик. Как его получить, мы
разберем далее, а пока сделаем так, чтобы BeanFactory
хранила в себе ClassLoader
нашего приложения.
В итоге сканирование классов в BeanFactory
будет выглядеть
следующим образом:
public class BeanFactory {
private ClassLoader classLoader;
private Map<String, Object> beans = new HashMap<>();
public BeanFactory(ClassLoader classLoader){
this.classLoader = classLoader;
}
public void instantiate(String basePackage){
try {
String path = basePackage.replace('.', '/');
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
File file = new File(resource.toURI());
parseFile(file, basePackage);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void parseFile(File baseFile, String basePackage) throws Exception {
for(File file : baseFile.listFiles()){
String fileName = file.getName();
if(file.isDirectory()){
String newBasePackage = basePackage + "." + fileName;
parseFile(file, newBasePackage);
}
else if(fileName.endsWith(".class")){
String className = fileName.substring(0, fileName.lastIndexOf("."));
Class<?> classObject = Class.forName(basePackage + "." + className);
parseClass(classObject);
}
}
}
private void parseClass(Class<?> classObject) throws Exception {
if (classObject.isAnnotationPresent(Component.class) ||
classObject.isAnnotationPresent(RestController.class)) {
Object instance = classObject.newInstance();
String className = classObject.getName();
String beanName = className.substring(0, 1).toLowerCase() + className.substring(1);
beans.put(beanName, instance);
}
}
}
В отличие от предыдущей версии я немного изменил алгоритм нахождения бинов.
В этой версии просканируются все файлы, которые находятся в базовом
пакете и во всех его подпакетах. Также классы с аннотацией RestController
будут считаться бинами. Чтобы компилятор не ругался на RestController
,
создайте эту аннотацию в пакете org.springframework.web.bind.annotation
:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RestController {
}
Далее в пакетеorg.springframework.web.context
создадим класс WebApplicationContext
,
на данный момент он пока будет выглядеть так:
public class WebApplicationContext {
public final static String BASE_PACKAGE_ATTRIBUTE = "basePackage";
private ServletContext servletContext;
private BeanFactory beanFactory;
public WebApplicationContext(ServletContext servletContext){
this.servletContext = servletContext;
beanFactory = new BeanFactory(servletContext.getClassLoader());
}
public Object getBean(String name){
return beanFactory.getBean(name);
}
}
Мы установим название пакета с классами клиента в качестве атрибута ServletContext
,
а именем этого атрибута будет являться BASE_PACKAGE_ATTRIBUTE
.Также именно с помощью
ServletContext
мы достанем загрузчик нашего приложения. В оригинале WebApplicationContext
является интерфейсом, который наследуется от интерфейса ApplicationContext
добавляя при этом один
единственный метод getServletContext()
.
Теперь вспомним немного про Enum
и добавим класс RequestMethod
в пакете
org.springframework.web.bind.annotation
:
public enum RequestMethod {
GET("GET"), POST("POST"), PUT("PUT"), DELETE("DELETE");
private String value;
RequestMethod(String value){
this.value = value;
}
public boolean matches(String method){
return value.equals(method);
}
}
В нашей версии мы будем использовать только 4 метода протокола HTTP. Предназначение
метода matches(String method)
я поясню чуть позже. Что ж, а теперь
пришло время для того, чтобы создать одну из самых основных аннотаций Spring MVC -
RequestMapping
. Опять же в пакете org.springframework.web.bind.annotation
прописываем:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RequestMapping {
String[] path() default "";
RequestMethod[] methods() default {RequestMethod.GET, RequestMethod.POST,
RequestMethod.PUT, RequestMethod.DELETE};
}
Реализуем только два поля аннотации RequestMapping
. В качестве тренировки можете добавить
поддержку поля String[] consumes()
. Если что, то все аннотации типа GetMapping
, PostMapping
и т.п., просто являются более короткой записью аннотации RequestMapping
. Далее создадим класс
HandlerMapping
в пакете org.springframework.web.servlet
. Добавим ему метод isHandler(Class<?> beanType)
,
который будет выглядеть следующим образом:
public class HandlerMapping {
private final Map<RequestMappingInfo, HandlerMethod> mappingLookup = new HashMap<>();
public boolean isHandler(Class<?> beanType){
return beanType.isAnnotationPresent(RestController.class)
|| beanType.isAnnotationPresent(RequestMapping.class);
}
}
Тело метода я почти полностью взял из его оригинальной реализации, там только
проверяется аннотация Controller
вместо RestController
. В некоторых статьях
я встречал мнение, что можно заменить аннотацию Controller
на аннотацию
Component
и все будет работать корректно. На самом деле это не совсем верно.
Смотря на тело метода можно понять, что если над контроллером поставить
аннотацию Component
, но не поставить аннотацию RequestMapping
, ваши методы не будут
обрабатывать приходящие запросы. Далее создадим следующие два класса.
Один из них будет содержать в себе информацию, которую мы извлечем из аннотации RequestMapping
.
Называться он будет соответственным образом -RequestMappingInfo
, будет располагаться
в пакете org.springframework.web.servlet.mvc.method
:
public class RequestMappingInfo {
private Set<String> patterns;
private Set<RequestMethod> methods;
public RequestMappingInfo(String[] patterns, RequestMethod[] methods) {
this.patterns = Arrays.stream(patterns).collect(Collectors.toSet());
this.methods = Arrays.stream(methods).collect(Collectors.toSet());
}
}
Пока выглядит вот так, потом мы еще добавим несколько методов. Вторым классом
будет являться HandlerMethod
из пакета org.springframework.web.method
:
public class HandlerMethod {
private Method method;
private Object bean;
private Parameter[] methodParameters;
public HandlerMethod(Method method, Object bean) {
this.method = method;
this.bean = bean;
methodParameters = method.getParameters();
}
}
То есть данный класс содержит в себе ссылку на метод обработчик, а также на контроллер, в котором этот
метод определён. Также в поля класса сохраним параметры метода, потом они нам пригодятся.
Наконец добавим в класс HandlerMapping
метод, который будет находить в контроллере все методы,
аннотированные RequestMapping
:
public void detectHandlerMethods(Object handler){
Class<?> beanType = handler.getClass();
RequestMapping beanRequestMapping = beanType.getAnnotation(RequestMapping.class);
RequestMappingInfo beanInfo = null;
if(beanRequestMapping != null){
beanInfo = new RequestMappingInfo(beanRequestMapping.path(), beanRequestMapping.methods());
}
for(Method method : beanType.getDeclaredMethods()){
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
if(requestMapping != null){
RequestMappingInfo info =
new RequestMappingInfo(requestMapping.path(), requestMapping.methods());
if(beanInfo != null){
info = info.combine(beanInfo);
}
HandlerMethod handlerMethod = new HandlerMethod(method, handler);
mappingLookup.put(info, handlerMethod);
}
}
}
На этом методе остановимся поподробнее. Для начала нам нужно проверить наличие аннотации RequestMapping
над самим контроллером и, если она присутствует, то на её основе создать объект класса RequestMappingInfo
.
Далее с помощью метода getDeclaredMethods()
достаем все методы данного контроллера, то есть методы того
же класса Object
мы не получим. Есть также метод getMethods()
, который возвращает
все public
методы данного класса и его суперкласса. Получив все методы контроолера просто проверяем наличие
аннотации RequestMapping
и на её основе создаем объекты класса RequestMappingInfo
. Теперь самое интересное,
нам нужно объединить информацию из аннотации RequestMapping
, которая стоит над классом, с аннотацией, которая
находится над методом. Для этого добавим метод combine(RequestMappingInfo beanInfo)
в класс
RequestMappingInfo
:
public RequestMappingInfo combine(RequestMappingInfo beanInfo){
Set<String> newPatterns = new HashSet<>();
for(String beanPattern : beanInfo.patterns){
for(String pattern : patterns)
newPatterns.add(beanPattern + pattern);
}
HashSet<RequestMethod> newMethods = new HashSet<>(methods);
newMethods.addAll(beanInfo.methods);
return new RequestMappingInfo(newPatterns, newMethods);
}
Во-первых, нам нужно к паттернам URI, которые обрабатывает метод,
добавить в качестве префикса паттерны URI контроллера. Для этого воспользуемся обычной конкатенацией строк.
Во-вторых, нам нужно объединить RequestMethod-ы контроллера и самого метода обработчика.
На основе полученных данных возвращаем новый объект. Возвращаясь к методу detectHandlerMethods(Object handler)
,
после получения нового объекта RequestMappingInfo
создаем объект класса HandlerMethod
и сохраняем эти
два объекта в мапу, где ключом выступает RequestMappingInfo
.
Теперь давайте добавим в класс RequestMappingInfo
метод, который будет проверять, удовлетворяет ли данный объект
переданному запросу:
public boolean matches(String path, HttpServletRequest request) {
String method = request.getMethod();
if(methods.stream().noneMatch(s -> s.matches(method)))
return false;
return patterns.stream().anyMatch(path::matches);
}
Сначала мы проверяем метод запроса, как раз здесь нам и понадобился метод matches(String method)
класса RequestMethod
, так как метод getMethod()
возвращает название метода запроса заглавными
буквами. Также в качестве параметра будем принимать путь запрашиваемого ресурса относительно
нашего приложения. Далее мы проверяем удовлетворяет хотя бы один паттерн из множества patterns
данному запросу. Пусть в нашем приложении паттерны можно будет задавать в качестве регулярных выражений.
Поэтому будем использовать стандартный метод matches(String regex)
класса String
. По хорошему
нужно создавать объекты класса Pattern
во время
старта приложения для каждого объекта из множества patterns
, а "скомпилированные" паттерны сохранять в поле
класса. Данное решение обладает двумя плюсами. Во-первых, мы сможем проверить валидность паттерна во время старта
приложения, а не во время обработки запроса. При невалидном регулярном выражении бросаем исключение, не
давая приложению запуститься. Во-вторых, сама процедура создания объекта класса Pattern
достаточно дорогая. Пусть
уж лучше приложение запускается дольше, но обработка самих запросов будет быстрее. В нашей реализации мы этого делать
не будем, чтобы, скажем так, вспомогательного кода было поменьше. После этого добавим в
класс HandlerMapping
следующий метод:
private HandlerMethod lookupHandlerMethod(String path, HttpServletRequest request){
for(RequestMappingInfo info : mappingLookup.keySet()){
if(info.matches(path, request)){
return mappingLookup.get(info);
}
}
return null;
}
То есть мы проходимся по всем ключам мапы mappingLookup
и ищем RequestMappingInfo
, удовлетворяющий
текущему запросу. Затем просто возвращаем HandlerMethod
соответствующий найденному ключу. Также стоит
здесь упомянуть, что оригинальный спрниг не сразу возвращает метод обработчик. Он ищет все методы, которые
удовлетворяют этому запросу, а потом выбирает из них тот, который более уникальный. К примеру, если у нас
есть два метода. Один из них обрабатывает запросы, приходящие на /root/test
, а второй - /root/*
.
Как вы уже догадались, будет вызван именно первый метод, так как паттерн более уникальный.
Мы этого реализовывать не будем, так как это достаточно непростая задача. Наконец добавим
public
метод, который мы будем вызывать непосредственно из класса DispatcherServlet
:
public HandlerMethod getHandler(HttpServletRequest request){
String requestURI = request.getRequestURI();
String lookupPath = requestURI.substring(request.getContextPath().length());
return lookupHandlerMethod(lookupPath, request);
}
Также в этом методе определим относительный путь запрашиваемого ресурса. Метод getRequestURI()
вернет строку вида /web-app/test
, где /web-app
- context path нашего приложения.
Обычно context path-ом является название war файла приложения. То есть в итоге lookupPath
будет равен /test.
После того, как мы нашли метод обработчик для текущего запроса, нам нужно его вызвать. Но для того, чтобы
это сделать, нам сначала нужно найти аргументы этого метода. Этим занимаются классы, реализующие интерфейс
HandlerMethodArgumentResolver
из пакета org.springframework.web.method.support
. Данный интерфейс выглядит
следующим образом:
public interface HandlerMethodArgumentResolver {
boolean supports(Parameter parameter);
Object resolveArgument(Parameter parameter, HttpServletRequest request, HttpServletResponse response);
}
Спринг по умолчанию имеет немалое количество "резолверов", но, если вы захотели создать свой собственный,
то реализуйте этот интерфейс, а потом в конфигурации приложения добавьте его с помощью метода
addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers)
класса WebMvcConfigurerAdapter
.
Для каждого типа параметра метода обработчика существует свой HandlerMethodArgumentResolver
. Если есть желание ознакомиться
со всеми типами параметров, который может принимать ваш метод, можете заглянуть сюда.
В нашей версии параметром метода может быть HttpServletRequest
и строка, являющаяся параметром запроса.
Значение строки мы достанем с помощью аннотации RequestParam
из пакета org.springframework.web.bind.annotation
:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParam {
String name();
String defaultValue() default "null";
}
Как видите, в качестве "таргета" мы указали только параметр метода. Так как null
не может являться значением по
умолчанию, у нас будет просто строка null. По хорошему нужно создать отдельную константу для этого, что, кстати,
и делают Juergen Hoeller и компания. Далее давайте добавим класс RequestParamMethodArgumentResolver
,
который будет обрабатывать параметры метода с аннотацией RequestParam
. Данный класс находится в пакете
org.springframework.web.method.annotation
:
public class RequestParamMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supports(Parameter parameter) {
RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
return requestParam != null;
}
@Override
public Object resolveArgument(Parameter parameter, HttpServletRequest request,
HttpServletResponse response) {
RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
String arg = request.getParameter(requestParam.name());
return arg != null ? arg : requestParam.defaultValue();
}
}
Я думаю, что здесь все понятно. Также добавим "резолвера" для объекта запроса, он будет находится в том же пакете
org.springframework.web.method.annotation
и будем называться соответственным образом - HttpRequestMethodArgumentResolver
:
public class HttpRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supports(Parameter parameter) {
return HttpServletRequest.class.isAssignableFrom(parameter.getType());
}
@Override
public Object resolveArgument(Parameter parameter, HttpServletRequest request,
HttpServletResponse response) {
return request;
}
}
Данный класс получился еще проще предыдущего. Метод isAssignableFrom(Class<?> clazz)
проверяет
является ли класс HttpServletRequest
суперинтерфейсом для класса параметра. Теперь пусть класс
HandlerMethod
будет содержать в себе следующий метод:
private Object[] getMethodArgumentValues(HttpServletRequest request, HttpServletResponse response){
if(methodParameters.length == 0)
return new Object[0];
Object[] args = new Object[methodParameters.length];
for(int i = 0; i < args.length; i++){
Parameter parameter = methodParameters[i];
for(HandlerMethodArgumentResolver argumentResolver : resolvers){
if(argumentResolver.supports(parameter)){
args[i] = argumentResolver.resolveArgument(parameter, request, response);
break;
}
}
}
return args;
}
Как понятно из названия, данный метод непосредственно будет определять аргументы для метода обработчика.
Если помните, то массив methodParameters
мы заполнили ещё в конструкторе. resolvers
- это обычный лист, который
мы присвоим с помощью сетера. Наконец добавим публичный метод для вызова обработчика:
public Object invokeAndHandle(HttpServletRequest request, HttpServletResponse response){
Object[] args = getMethodArgumentValues(request, response);
Object result = null;
try {
result = method.invoke(bean, args);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
Ничего сложного, сначала определяем аргументы метода, далее вызываем его и возвращаем результат
работы. Сам DispatcherServlet
не будет вызывать метод обработчик, этим займется класс HandlerAdapter
из пакета org.springframework.web.servlet.mvc.method.annotation
:
public class HandlerAdapter {
private List<HandlerMethodArgumentResolver> argumentResolvers;
public HandlerAdapter(List<HandlerMethodArgumentResolver> argumentResolvers) {
this.argumentResolvers = argumentResolvers;
}
public void invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response,
HandlerMethod handlerMethod) throws IOException {
handlerMethod.setResolvers(argumentResolvers);
Object result = handlerMethod.invokeAndHandle(request, response);
response.getWriter().println(result);
}
}
В методе invokeHandlerMethod
мы запускаем метод обработчик и сразу отправляем результат клиенту.
Также, как вы могли заметить, данный класс будет хранить в себе все "резолверы".
Что ж, пришло время создать точку входа в наше приложение. Добавьте класс DispatcherServlet
в пакет
org.springframework.web.servlet
:
public class DispatcherServlet extends HttpServlet {
public final static String HANDLER_MAPPING_BEAN_NAME = "handlerMapping";
public final static String HANDLER_ADAPTER_BEAN_NAME= "handlerAdapter";
private WebApplicationContext webApplicationContext;
private HandlerMapping handlerMapping;
private HandlerAdapter handlerAdapter;
public DispatcherServlet(WebApplicationContext context){
webApplicationContext = context;
}
private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws IOException {
HandlerMethod handler = handlerMapping.getHandler(request);
if(handler == null)
response.sendError(HttpServletResponse.SC_NOT_FOUND);
else
handlerAdapter.invokeHandlerMethod(request, response, handler);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doDispatch(req, resp);
}
}
Как видите, методы типа doX
будут просто делегировать запрос методу doDispatch
. Этот метод сначала
будет искать метод обработчик, который удовлетворяет текущему запросу. Если не удалось найти такой метод, то
просто будем возвращать клиенту ошибку 404. Если же метод найден, то вызываем его с помощью handlerAdapter
.
Также добавим имена бинов, которые мы будем использовать дальше для инициализации.
Мы добавили поля handlerMapping
и handlerAdapter
, но не инициализировали их, пришло время это сделать.
В класс WebApplicationContext
нужно добавить следующие методы. Один из них это метод для инициализации HandlerMapping
:
private HandlerMapping initHandlerMapping(){
HandlerMapping handlerMapping = new HandlerMapping();
for (Object bean : beanFactory.getBeans().values()) {
Class<?> beanType = bean.getClass();
if(handlerMapping.isHandler(beanType))
handlerMapping.detectHandlerMethods(bean);
}
return handlerMapping;
}
То есть мы проверяем является ли бин контроллером, если является, то пытаемся найти в нем все методы
обработчики. Далее создадим метод для инициализации HandlerAdapter
:
private HandlerAdapter initHandlerAdapter(){
return new HandlerAdapter(initArgumentResolvers());
}
private List<HandlerMethodArgumentResolver> initArgumentResolvers(){
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
resolvers.add(new RequestParamMethodArgumentResolver());
resolvers.add(new HttpRequestMethodArgumentResolver());
return resolvers;
}
Сначала создаем всех доступных "резолверов", а потом на их основе создаем HandlerAdapter
.
Теперь инициализируем нашу фабрику и добавим руками еще два бина HandlerMapping
и HandlerAdapter
:
public void init(){
String basePackage = (String) servletContext.getAttribute(BASE_PACKAGE_ATTRIBUTE);
beanFactory.instantiate(basePackage);
beanFactory.addBean(DispatcherServlet.HANDLER_MAPPING_BEAN_NAME, initHandlerMapping());
beanFactory.addBean(DispatcherServlet.HANDLER_ADAPTER_BEAN_NAME, initHandlerAdapter());
}
А теперь осталось вызвать этот метод в DispatcherServlet
и достать наших "хэндлеров":
@Override
public void init() {
webApplicationContext.init();
handlerMapping =
(HandlerMapping) webApplicationContext.getBean(HANDLER_MAPPING_BEAN_NAME);
handlerAdapter =
(HandlerAdapter) webApplicationContext.getBean(HANDLER_ADAPTER_BEAN_NAME);
}
Данный метод будет вызван контейнером при старте приложения и произведёт всю нужную нам инициализацию.
До того, как появилась 3 версия Servlet API, регистрацию DispatcherServlet-a
осуществляли только с помощью web.xml
, но в 3 версии добавили возможность регистрации
через Java код. В нашей реализации мы будем производить регистрацию именно с помощью кода.
Создадим класс WebApplicationInitializer
в пакете org.springframework.web
:
public abstract class WebApplicationInitializer {
public void onStartup(ServletContext context) {
context.setAttribute(WebApplicationContext.BASE_PACKAGE_ATTRIBUTE, getPackageToScan());
WebApplicationContext applicationContext = new WebApplicationContext(context);
DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
ServletRegistration.Dynamic registration =
context.addServlet("dispatcherServlet", dispatcherServlet);
registration.setLoadOnStartup(1);
registration.addMapping(getServletMapping());
}
protected abstract String getServletMapping();
protected abstract String getPackageToScan();
}
Сделаем два абстрактных метода, чтобы клиент мог указать мапинг для DispatcherServlet-a и пакет, в котором находятся пользовательские классы. Установим название пакета с классами клиента в качестве атрибута ServletContext-а. Также укажем диспатчер сервлету, чтобы его инициализация происходила во время старта приложения.
В реальном веб-приложении может возникнуть нужда в создании еще одного DispatcherServlet-a, но с другой
конфигурацией, то есть получается, что у них должен быть разный WebApplicationContext
. Но также помимо
контроллеров, "резолверов" аргументов и т.п., в приложении должны быть бины, отвечающие за его бизнес-логику.
Было бы странно, если бы один бин существовал в двух экземплярах. Во избежание этой проблемы, при старте
приложения спринг создает два контекста. Один, так называемый, RootContext
, где хранятся
бины отвечающие за логику приложения. А второй WebContext
, где хранятся бины для обработки
запросов. RootContext
является родителем WebContext
, поэтому дублирования бинов не происходит.
Также RootContext
нужен для того, чтобы связать жизненный цикл спринг приложения, с жизненным циклом
ServletContext-а. Например, при завершении работы приложения, контейнер вызывает метод
contextDestroyed(ServletContextEvent event)
всех слушателей, которые реализуют интерфейс
ServletContextListener
. Как раз в этом методе и должен "закрываться" контекст спринг приложения.
В нашей реализации мы оставим один контекст, так как у нас простой случай и один DispatcherServlet
.
Вернемся к нашей инициализации. Возникает вопрос, а как сделать так, чтобы
контейнер при старте приложения вызвал наш метод onStartup(ServletContext context)
? После прочтения предыдущего абзаца,
первая мысль, которая может возникнуть, это использовать ServletContextListener
и в методе contextInitialized(ServletContextEvent event)
производить всю инициализацию. Но в спринге не используется данный подход, как минимум, из-за одного недостатка.
Так как в оригинальном спринге RootContext
создается именно с помощью слушателя ContextLoaderListener
,
нужно чтобы он отрабатывал раньше всех слушателей, которые может добавить клиент. Поэтому при старте
приложения спринг добавляет своего слушателя, а слушатели, которые реализуют интерфейс ServletContextListener
будут
запускаться в том порядке, в котором они были добавлены. Так как метод onStartup(Set<Class<?>> classes, ServletContext context)
(см. ниже)
отработает раньше всех слушателей, контекст будет уже создан к началу работы слушателей клиента. Также порядок запуска
слушателей можно указать в web.xml
, но мы же не хотим заставлять клиента использовать xml.
Как и в оригинальном спринге мы будем использовать механизм SPI, который стал доступен начиная с 3 версии Servlet API.
Подробнее про SPI можно прочесть здесь(английский)
и здесь(русский). Во-первых, создадим класс, который будет реализовывать
интерфейс ServletContainerInitializer
. В пакет org.springframework.web
добавим класс SpringServletContainerInitializer
:
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> classes, ServletContext context) {
if (classes != null) {
for(Class<?> clazz : classes){
try {
WebApplicationInitializer initializer =
(WebApplicationInitializer) clazz.newInstance();
initializer.onStartup(context);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
Самое интересное для нас здесь - аннотация HandlesTypes
. Во время старта приложения контейнер будет
искать в war файле все классы, которые реализуют абстрактный класс WebApplicationInitializer
, все найденные
классы он передаст в качестве множества classes
. Дальше мы уже сами руками вызываем метод
onStartup(ServletContext context)
. Наконец создаем файл с названием javax.servlet.ServletContainerInitializer
в директории resources/META-INF/services/
. В нем прописываем полное имя класса, который реализует интерфейс
ServletContainerInitializer
, в нашем случае это будет org.springframework.web.SpringServletContainerInitializer
.
Что ж, настало время проверить написанное. Создайте пакет com.client
, а в нем класс AppInitializer
:
public class AppInitializer extends WebApplicationInitializer {
@Override
protected String getServletMapping() {
return "/";
}
@Override
protected String getPackageToScan() {
return "com.client";
}
}
Далее осталось только добавить контроллер. В пакете com.client.controller
создадим класс
GreetingController
:
@RestController
@RequestMapping(path = "/root", methods = RequestMethod.POST)
public class GreetingController {
@RequestMapping(path = "/greeting", methods = RequestMethod.GET)
public String test(HttpServletRequest request,
@RequestParam(name = "name", defaultValue = "user") String name){
String requestName = Optional.ofNullable(request.getParameter("name")).
orElse("user");
if(!name.equals(requestName))
throw new IllegalArgumentException();
return "Hello " + name;
}
}
Поехали! Запускаем сервер, начинаем тестировать. Сначала отправим GET
и POST
запросы
на ресурс /root/greeting?name=Nick
:
Теперь попробуем отправить PUT
запрос на тот же адрес:
Все как и ожидалось. Сервер нам вернул 404, так как мы обрабатываем только GET
и POST
методы.
Наконец отправим GET
запрос без параметров:
Работает корректно. В принципе, вы еще можете поиграться, к примеру, потестировать регулярные выражения или указать несколько паттернов для одного метода. А я, пожалуй, на этом закончу. Надеюсь, что статья оказалась для вас полезной, и вы открыли для себя что-то новое.