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

Помимо веб-служб 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-файлом, структура каталогов должна выглядеть следующим образом:

src/
 +- 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/**")
            }
        }
    }
}