Шаблонізатори

Орім вебслужб REST, можна також використовувати Spring MVC для обробки динамічного вмісту HTML. Spring MVC підтримує різні технології шаблонизації, включно з Thymeleaf, FreeMarker та JSP. До того ж, багато інших шаблонізаторів містять власні засоби інтеграції зі Spring MVC.

Spring Boot передбачає підтримку автоконфігурації для наступних шаблонизаторів:

Якщо один із цих шаблонизаторів використовується зі стандартною конфігурацією, шаблони автоматично підбираються з src/main/ resources/templates.

Залежно від того, яким чином виконується програма, IDE може по-різному впорядковувати classpath. Виконання програми в IDE з його основного методу призводить до іншого впорядкування, ніж при виконанні програми за допомогою Maven або Gradle, або з упакованого jar-файлу. Це може призвести до того, що Spring Boot не зможе знайти очікуваний шаблон. Якщо виникла така проблема, можна змінити впорядкування classpath в IDE, щоб класи і ресурси модуля йшли першими.

Обробка помилок

За замовчуванням Spring Boot передбачає відображення /error, яке обробляє всі помилки адекватним чином, і воно реєструється як "глобальна" сторінка помилок у контейнері сервлетів. Для машинних клієнтів воно створює відповідь у форматі JSON з докладним описом помилки, кодом стану HTTP та повідомленням про виключення. Для браузерних клієнтів існує подання помилок "whitelabel", яке візуалізує ті ж дані у форматі HTML (щоб налаштувати його, додай View, що дозволяє error).

Існує низка властивостей server.error, які можна встановити, якщо потрібно персоналізувати налаштування логіки обробки помилок за замовчуванням.

Щоб повністю замінити логіку роботи за замовчуванням, можна реалізувати ErrorController та зареєструвати визначення біну цього типу або додати бін типу ErrorAttributes, щоб використовувати існуючий механізм, але замінити вміст.

BasicErrorController можна використовувати як базовий клас для кастомного ErrorController. Це особливо практично, якщо потрібно додати обробник для нового типу вмісту (за замовчуванням обробник обробляє тільки text/html і передає запасний варіант для всього іншого). Для цього розшир BasicErrorController, додай публічний метод з анотацією @RequestMapping, що має атрибут produces, та створи бін нового типу.

Окрім цього, можна визначити клас, анотований @ControllerAdvice, щоб налаштувати JSON-документ, що повертається, під певний контролер і/або тип виключення, як показано в наступному прикладі:

Java

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice(basePackageClasses = SomeController.class)
public class MyControllerAdvice extends ResponseEntityExceptionHandler {
    @ResponseBody
    @ExceptionHandler(MyException.class)
    public ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) {
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(new MyErrorBody(status.value(), ex.getMessage()), status);
    }
    private HttpStatus getStatus(HttpServletRequest request) {
        Integer code = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        HttpStatus status = HttpStatus.resolve(code);
        return (status != null) ? status : HttpStatus.INTERNAL_SERVER_ERROR;
    }
}
Kotlin
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
import javax.servlet.RequestDispatcher
import javax.servlet.http.HttpServletRequest
@ControllerAdvice(basePackageClasses = [SomeController::class])
class MyControllerAdvice : ResponseEntityExceptionHandler() {
    @ResponseBody
    @ExceptionHandler(MyException::class)
    fun handleControllerException(request: HttpServletRequest, ex: Throwable): ResponseEntity<*> {
        val status = getStatus(request)
        return ResponseEntity(MyErrorBody(status.value(), ex.message), status)
    }
    private fun getStatus(request: HttpServletRequest): HttpStatus {
        val code = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE) as Int
        val status = HttpStatus.resolve(code)
        return status ?: HttpStatus.INTERNAL_SERVER_ERROR
    }
}

У попередньому прикладі, якщо MyException генерується контролером, визначеним у тому ж пакеті, що і SomeController, замість подання ErrorAttributes використовується JSON-подання POJO-об'єкта MyErrorBody.

У деяких випадках помилки, оброблені на рівні контролера, не реєструються інфраструктурою метрик. Програми можуть забезпечити запис таких винятків до метрики запиту, налаштувавши оброблений виняток як атрибут запиту:

Java
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
@Controller
public class MyController {
    @ExceptionHandler(CustomException.class)
    String handleCustomException(HttpServletRequest request, CustomException ex) {
        request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex);
        return "errorView";
    }
}
Kotlin

import org.springframework.boot.web.servlet.error.ErrorAttributes
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler
import javax.servlet.http.HttpServletRequest
@Controller
class MyController {
    @ExceptionHandler(CustomException::class)
    fun handleCustomException(request: HttpServletRequest, ex: CustomException?): String {
        request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex)
        return "errorView"
    }
}

Кастомні сторінки помилок

Якщо необхідно вивести на екран кастомну HTML-сторінку помилки для вказаного коду стану, можна додати файл до каталогу /error. Сторінки помилок можуть бути статичними HTML (тобто доданими в будь-який з каталогів статичних ресурсів), або побудованими за допомогою шаблонів. Ім'я файлу має бути точним кодом стану або маскою серії.

Наприклад, щоб відобразити 404 зі статичним HTML-файлом, структура каталогів має виглядати так:

 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

Щоб відобразити всі помилки 5xx за допомогою шаблону FreeMarker, структура каталогу повинна бути наступною:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.ftlh
             +- <other templates>

Для більш складних відображень можна також додати біни, що реалізують інтерфейс ErrorViewResolver, як показано в наступному прикладі:

Java

import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
public class MyErrorViewResolver implements ErrorViewResolver {
    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
         // Використовуємо запит бажання повернути ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // Тут ми можемо додати кастомне значення моделі
            new ModelAndView("myview");
        }
        return null;
    }
}
Kotlin

import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver
import org.springframework.http.HttpStatus
import org.springframework.web.servlet.ModelAndView
import javax.servlet.http.HttpServletRequest
class MyErrorViewResolver : ErrorViewResolver {
    override fun resolveErrorView(request: HttpServletRequest, status: HttpStatus,
            model: Map<String, Any>): ModelAndView? {
        // Використовуємо запит або статус, щоб за бажанням повернути ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // Тут ми можемо додати кастомне значення моделі
            return ModelAndView("myview")
        }
        return null
    }
}

Також можна використовувати звичайні функції Spring MVC, такі як методи, позначені анотацією @ExceptionHandler та анотацію @ControllerAdvice. ErrorController потім підхоплює всі необроблені винятки.

Відображення сторінок помилок поза Spring MVC

У випадку з додатками, які не використовують Spring MVC, можна використовувати інтерфейс ErrorPageRegistrar для прямої реєстрації ErrorPages. Ця абстракція працює безпосередньо з базовим вбудованим контейнером сервлетів і добре себе показує, навіть якщо у тебе немає DispatcherServlet для Spring MVC.

Java
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
@Configuration(proxyBeanMethods = false)
public class MyErrorPagesConfiguration {
    @Bean
    public ErrorPageRegistrar errorPageRegistrar() {
        return this::registerErrorPages;
    }
    private void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
    }
}
Kotlin
import org.springframework.boot.web.server.ErrorPage
import org.springframework.boot.web.server.ErrorPageRegistrar
import org.springframework.boot.web.server.ErrorPageRegistry
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpStatus
@Configuration(proxyBeanMethods = false)
class MyErrorPagesConfiguration {
    @Bean
    fun errorPageRegistrar(): ErrorPageRegistrar {
        return ErrorPageRegistrar { registry: ErrorPageRegistry -> registerErrorPages(registry) }
    }
    private fun registerErrorPages(registry: ErrorPageRegistry) {
        registry.addErrorPages(ErrorPage(HttpStatus.BAD_REQUEST, "/400"))
    }
}
Якщо ти реєструєш ErrorPage із використанням шляху, який в кінцевому підсумку обробляється за допомогою Filter (що характерно для деяких вебфреймворків, що не належать до Spring, таких як Jersey і Wicket), то Filter має бути явно зареєстрований як диспетчер ERROR, як показано в наступному приклад:
Java

import java.util.EnumSet;
import javax.servlet.DispatcherType;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class MyFilterConfiguration {
    @Bean
    public FilterRegistrationBean<MyFilter> myFilter() {
        FilterRegistrationBean<MyFilter> registration = new FilterRegistrationBean<>(new MyFilter());
        // ...
        registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
        return registration;
    }
}
Kotlin
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.EnumSet
import javax.servlet.DispatcherType
@Configuration(proxyBeanMethods = false)
class MyFilterConfiguration {
    @Bean
    fun myFilter(): FilterRegistrationBean<MyFilter> {
        val registration = FilterRegistrationBean(MyFilter())
        // ...
        registration.setDispatcherTypes(EnumSet.allOf(DispatcherType::class.java))
        return registration
    }
}

Зверни увагу, що FilterRegistrationBean за замовчуванням не містить тип диспетчера ERROR.

Обробка помилок під час WAR-розгортання

При розгортанні в контейнері сервлетів Spring Boot використовує свій фільтр сторінки помилок для перенаправлення запиту з кодом помилки на відповідну сторінку помилок. Це необхідно, оскільки специфікація сервлетів не передбачає API для реєстрації сторінок помилок. Залежно від контейнера, в якому ти розгортаєш свій war-файл, і технологій, які використовують твою програму, може знадобитися додаткове конфігурування.

Фільтр сторінки помилки зможе перенаправити запит на правильну сторінку помилки тільки в тому випадку, якщо відповідь ще не була зафіксована. За замовчуванням WebSphere Application Server 8.0 та пізніших версій фіксує відповідь після успішного завершення методу служби сервлета. Слід вимкнути цю логіку роботи, встановивши для параметра com.ibm.ws.webcontainer.invokeFlushAfterService значення false.

Якщо ти використовуєш Spring Security і хочеш отримати доступ до принципала на сторінці помилки, то необхідно налаштувати фільтр Spring Security так, щоб він викликався під час відправлення помилок. Для цього встанови властивість spring.security.filter.dispatcher-types у async, error, forward, request.

Підтримка CORS

Спільне використання ресурсів між різними джерелами (CORS) є специфікацією W3C, реалізованою більшістю браузерів, яка дозволяє гнучко визначати, які типи міждоменних запитів будуть авторизовані, і вона покликана замінити деякі менш безпечні та менш ефективні підходи, такі як IFRAME або JSONP.

Починаючи з версії 4.2, Spring MVC підтримує CORS. Використання методу контролера для конфігурації CORS з анотаціями @CrossOrigin у додатку Spring Boot не вимагає будь-якої спеціальної конфігурації. Глобальну конфігурацію CORS можна визначити шляхом реєстрації біна WebMvcConfigurer з налаштованим методом addCorsMappings(CorsRegistry), як показано в наступному прикладі:

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration(proxyBeanMethods = false)
public class MyCorsConfiguration {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**");
            }
        };
    }
}
Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration(proxyBeanMethods = false)
class MyCorsConfiguration {
    @Bean
    fun corsConfigurer(): WebMvcConfigurer {
        return object : WebMvcConfigurer {
            override fun addCorsMappings(registry: CorsRegistry) {
                registry.addMapping("/api/**")
            }
        }
    }
}