«1. Введение

Проще говоря, в шаблоне проектирования Front Controller один контроллер отвечает за направление входящих запросов HttpRequest ко всем другим контроллерам и обработчикам приложения.

Spring DispatcherServlet реализует этот шаблон и, следовательно, отвечает за правильную координацию HttpRequests с их правильными обработчиками.

В этой статье мы рассмотрим рабочий процесс обработки запросов Spring DispatcherServlet и способы реализации нескольких интерфейсов, участвующих в этом рабочем процессе.

2. Обработка запроса DispatcherServlet

По сути, DispatcherServlet обрабатывает входящий HttpRequest, делегирует запрос и обрабатывает этот запрос в соответствии с настроенными интерфейсами HandlerAdapter, которые были реализованы в приложении Spring вместе с сопутствующими аннотациями, определяющими обработчики, контроллер конечные точки и объекты ответа.

Давайте более подробно рассмотрим, как DispatcherServlet обрабатывает компонент:

    WebApplicationContext, связанный с DispatcherServlet под ключом DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, ищется и становится доступным для всех элементов процесса DispatcherServlet находит все реализации интерфейса HandlerAdapter, настроенные для вашего диспетчера с помощью getHandler() — каждая найденная и настроенная реализация обрабатывает запрос через handle() до конца процесса. LocaleResolver необязательно привязан к запросу, чтобы позволить элементам в процессе разрешаться локаль ThemeResolver дополнительно привязывается к запросу, чтобы позволить элементам, таким как представления, определять, какую тему использовать, если указан MultipartResolver, запрос проверяется на наличие MultipartFiles — все найденные объекты помещаются в MultipartHttpServletRequest для дальнейшей обработки. Реализации HandlerExceptionResolver объявленный в выборе WebApplicationContext исключения, возникающие при обработке запроса

Вы можете узнать больше обо всех способах регистрации и настройки DispatcherServlet здесь.

3. Интерфейсы HandlerAdapter

Интерфейс HandlerAdapter упрощает использование контроллеров, сервлетов, запросов HttpRequest и путей HTTP через несколько конкретных интерфейсов. Таким образом, интерфейс HandlerAdapter играет важную роль на многих этапах рабочего процесса обработки запросов DispatcherServlet.

Сначала каждая реализация HandlerAdapter помещается в цепочку HandlerExecutionChain из метода getHandler() вашего диспетчера. Затем каждая из этих реализаций обрабатывает() объект HttpServletRequest по мере выполнения цепочки выполнения.

В следующих разделах мы более подробно рассмотрим несколько наиболее важных и часто используемых HandlerAdapters.

3.1. Сопоставления

Чтобы понять сопоставления, нам нужно сначала посмотреть, как аннотировать контроллеры, поскольку контроллеры очень важны для интерфейса HandlerMapping.

SimpleControllerHandlerAdapter позволяет явно реализовать контроллер без аннотации @Controller.

RequestMappingHandlerAdapter поддерживает методы, аннотированные аннотацией @RequestMapping.

Здесь мы сосредоточимся на аннотации @Controller, но также доступен полезный ресурс с несколькими примерами использования SimpleControllerHandlerAdapter.

Аннотация @RequestMapping устанавливает конкретную конечную точку, в которой обработчик будет доступен в связанном с ним WebApplicationContext.

Давайте рассмотрим пример контроллера, который предоставляет и обрабатывает конечную точку «/user/example»:

@Controller
@RequestMapping("/user")
@ResponseBody
public class UserController {
 
    @GetMapping("/example")
    public User fetchUserExample() {
        // ...
    }
}

Пути, указанные в аннотации @RequestMapping, управляются внутри через интерфейс HandlerMapping.

Структура URL-адресов, естественно, связана с самим DispatcherServlet и определяется отображением сервлета.

Таким образом, если DispatcherServlet сопоставлен с «/», то все сопоставления будут покрыты этим сопоставлением.

«Если, однако, вместо этого сопоставление сервлета будет «/dispatcher», то любые аннотации @RequestMapping будут относиться к этому корневому URL-адресу.

Помните, что «/» — это не то же самое, что «/*» для отображений сервлетов! «/» — это сопоставление по умолчанию, и все URL-адреса отображаются в зоне ответственности диспетчера.

‘€˜/*’ сбивает с толку многих новых разработчиков Spring. В нем не указывается, что все пути с одинаковым контекстом URL находятся в зоне ответственности диспетчера. Вместо этого он переопределяет и игнорирует другие сопоставления диспетчера. Таким образом, «/пример» будет отображаться как 404!

По этой причине «/*» не следует использовать, кроме как в очень ограниченных случаях (например, при настройке фильтра).

3.2. Обработка HTTP-запросов

Основной обязанностью DispatcherServlet является отправка входящих HttpRequests правильным обработчикам, указанным в аннотациях @Controller или @RestController.

Кстати, основное различие между @Controller и @RestController заключается в способе генерации ответа — @RestController также определяет @ResponseBody по умолчанию.

Рецензию, в которой мы углубляемся в контроллеры Spring, можно найти здесь.

3.3. Интерфейс ViewResolver

ViewResolver присоединяется к DispatcherServlet как параметр конфигурации объекта ApplicationContext.

ViewResolver определяет, какие представления обслуживаются диспетчером и откуда они обслуживаются.

Вот пример конфигурации, которую мы поместим в наш AppConfig для рендеринга JSP-страниц:

@Configuration
@EnableWebMvc
@ComponentScan("com.baeldung.springdispatcherservlet")
public class AppConfig implements WebMvcConfigurer {

    @Bean
    public UrlBasedViewResolver viewResolver() {
        UrlBasedViewResolver resolver
          = new UrlBasedViewResolver();
        resolver.setPrefix("/WEB-INF/view/");
        resolver.setSuffix(".jsp");
        resolver.setViewClass(JstlView.class);
        return resolver;
    }
}

Очень просто! Это состоит из трех основных частей:

  1. setting the prefix, which sets the default URL path to find the set views within
  2. the default view type which is set via the suffix
  3. setting a view class on the resolver which allows technologies like JSTL or Tiles to be associated with the rendered views

Один общий вопрос касается того, как точно связаны ViewResolver диспетчера и общая структура каталогов проекта. Давайте взглянем на основы.

Вот пример конфигурации пути для InternalViewResolver с использованием XML-конфигурации Spring:

<property name="prefix" value="/jsp/"/>

Для нашего примера предположим, что наше приложение размещено на:

http://localhost:8080/

Это адрес и порт по умолчанию для локального сервера Apache Tomcat.

Предполагая, что наше приложение называется dispatcherexample-1.0.0, наши представления JSP будут доступны из:

http://localhost:8080/dispatcherexample-1.0.0/jsp/

Путь для этих представлений в обычном проекте Spring с Maven следующий:

src -|
     main -|
            java
            resources
            webapp -|
                    jsp
                    WEB-INF

Расположение по умолчанию для представлений находится в WEB-INF. Путь, указанный для нашего InternalViewResolver в приведенном выше фрагменте, определяет подкаталог «src/main/webapp», в котором будут доступны ваши представления.

3.4. Интерфейс LocaleResolver

Основной способ настройки сеанса, запроса или информации о файлах cookie для нашего диспетчера — через интерфейс LocaleResolver.

CookieLocaleResolver — это реализация, позволяющая настраивать свойства приложения без сохранения состояния с помощью файлов cookie. Добавим его в AppConfig.

@Bean
public CookieLocaleResolver cookieLocaleResolverExample() {
    CookieLocaleResolver localeResolver 
      = new CookieLocaleResolver();
    localeResolver.setDefaultLocale(Locale.ENGLISH);
    localeResolver.setCookieName("locale-cookie-resolver-example");
    localeResolver.setCookieMaxAge(3600);
    return localeResolver;
}

@Bean 
public LocaleResolver sessionLocaleResolver() { 
    SessionLocaleResolver localeResolver = new SessionLocaleResolver(); 
    localeResolver.setDefaultLocale(Locale.US); 
    localResolver.setDefaultTimeZone(TimeZone.getTimeZone("UTC"));
    return localeResolver; 
}

SessionLocaleResolver позволяет выполнять настройку сеанса в приложении с отслеживанием состояния.

Метод setDefaultLocale() представляет географический, политический или культурный регион, тогда как setDefaultTimeZone() определяет соответствующий объект TimeZone для рассматриваемого компонента приложения.

Оба метода доступны в каждой из приведенных выше реализаций LocaleResolver.

3.5. Интерфейс ThemeResolver

Spring обеспечивает стилистическое оформление наших представлений.

Давайте посмотрим, как настроить наш диспетчер для обработки тем.

Во-первых, давайте настроим всю конфигурацию, необходимую для поиска и использования наших статических файлов темы. Нам нужно установить местоположение статического ресурса для нашего ThemeSource, чтобы настроить сами фактические темы (объекты темы содержат всю информацию о конфигурации, указанную в этих файлах). Добавьте это в AppConfig:

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**")
      .addResourceLocations("/", "/resources/")
      .setCachePeriod(3600)
      .resourceChain(true)
      .addResolver(new PathResourceResolver());
}

@Bean
public ResourceBundleThemeSource themeSource() {
    ResourceBundleThemeSource themeSource
      = new ResourceBundleThemeSource();
    themeSource.setDefaultEncoding("UTF-8");
    themeSource.setBasenamePrefix("themes.");
    return themeSource;
}

Запросы, управляемые DispatcherServlet, могут изменять тему с помощью указанного параметра, переданного в setParamName(), доступного в объекте ThemeChangeInterceptor. Добавить в AppConfig:

@Bean
public CookieThemeResolver themeResolver() {
    CookieThemeResolver resolver = new CookieThemeResolver();
    resolver.setDefaultThemeName("example");
    resolver.setCookieName("example-theme-cookie");
    return resolver;
}

@Bean
public ThemeChangeInterceptor themeChangeInterceptor() {
   ThemeChangeInterceptor interceptor
     = new ThemeChangeInterceptor();
   interceptor.setParamName("theme");
   return interceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(themeChangeInterceptor());
}

Следующий тег JSP добавляется в наше представление, чтобы отображался правильный стиль:

<link rel="stylesheet" href="${ctx}/<spring:theme code='styleSheet'/>" type="text/css"/>

«

http://localhost:8080/dispatcherexample-1.0.0/?theme=example

«Следующий URL-запрос отображает пример темы с использованием параметра «theme», переданного в наш настроенный ThemeChangeIntercepter:

3.6. Интерфейс MultipartResolver

@Bean
public CommonsMultipartResolver multipartResolver() 
  throws IOException {
    CommonsMultipartResolver resolver
      = new CommonsMultipartResolver();
    resolver.setMaxUploadSize(10000000);
    return resolver;
}

Реализация MultipartResolver проверяет запрос на составные части и упаковывает их в MultipartHttpServletRequest для дальнейшей обработки другими элементами в процессе, если найдена хотя бы одна составная часть. Добавьте в AppConfig:

@Controller
public class MultipartController {

    @Autowired
    ServletContext context;

    @PostMapping("/upload")
    public ModelAndView FileuploadController(
      @RequestParam("file") MultipartFile file) 
      throws IOException {
        ModelAndView modelAndView = new ModelAndView("index");
        InputStream in = file.getInputStream();
        String path = new File(".").getAbsolutePath();
        FileOutputStream f = new FileOutputStream(
          path.substring(0, path.length()-1)
          + "/uploads/" + file.getOriginalFilename());
        int ch;
        while ((ch = in.read()) != -1) {
            f.write(ch);
        }
        f.flush();
        f.close();
        in.close();
        modelAndView.getModel()
          .put("message", "File uploaded successfully!");
        return modelAndView;
    }
}

Теперь, когда мы настроили наш bean-компонент MultipartResolver, давайте настроим контроллер для обработки запросов MultipartFile:

Мы можем использовать обычную форму для отправки файла в указанную конечную точку. . Загруженные файлы будут доступны в «CATALINA_HOME/bin/uploads».

3.7. Интерфейс HandlerExceptionResolver

Spring HandlerExceptionResolver обеспечивает единую обработку ошибок для всего веб-приложения, отдельного контроллера или набора контроллеров.

@ControllerAdvice
public class ExampleGlobalExceptionHandler {

    @ExceptionHandler
    @ResponseBody 
    public String handleExampleException(Exception e) {
        // ...
    }
}

Чтобы обеспечить пользовательскую обработку исключений для всего приложения, создайте класс, аннотированный @ControllerAdvice:

Любые методы в этом классе, аннотированные @ExceptionHandler, будут доступны на каждом контроллере в зоне ответственности диспетчера.

@Controller
public class FooController{

    @ExceptionHandler({ CustomException1.class, CustomException2.class })
    public void handleException() {
        // ...
    }
    // ...
}

Реализации интерфейса HandlerExceptionResolver в ApplicationContext DispatcherServlet доступны для перехвата конкретного контроллера в зоне ответственности этого диспетчера всякий раз, когда @ExceptionHandler используется в качестве аннотации, а правильный класс передается в качестве параметра:

~~ ~ Метод handleException() теперь будет служить обработчиком исключений для FooController в нашем примере выше, если возникнет исключение CustomException1 или CustomException2.

Вот статья, в которой более подробно рассматривается обработка исключений в веб-приложении Spring.

4. Заключение

В этом руководстве мы рассмотрели DispatcherServlet Spring и несколько способов его настройки.