Spring mvc руководство

Spring Boot is well suited for web application development.
You can create a self-contained HTTP server by using embedded Tomcat, Jetty, Undertow, or Netty.
Most web applications use the spring-boot-starter-web module to get up and running quickly.
You can also choose to build reactive web applications by using the spring-boot-starter-webflux module.

If you have not yet developed a Spring Boot web application, you can follow the «Hello World!» example in the Getting started section.

1. Servlet Web Applications

If you want to build servlet-based web applications, you can take advantage of Spring Boot’s auto-configuration for Spring MVC or Jersey.

1.1. The “Spring Web MVC Framework”

The Spring Web MVC framework (often referred to as “Spring MVC”) is a rich “model view controller” web framework.
Spring MVC lets you create special @Controller or @RestController beans to handle incoming HTTP requests.
Methods in your controller are mapped to HTTP by using @RequestMapping annotations.

The following code shows a typical @RestController that serves JSON data:

Java

import java.util.List;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class MyRestController {

    private final UserRepository userRepository;

    private final CustomerRepository customerRepository;

    public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {
        this.userRepository = userRepository;
        this.customerRepository = customerRepository;
    }

    @GetMapping("/{userId}")
    public User getUser(@PathVariable Long userId) {
        return this.userRepository.findById(userId).get();
    }

    @GetMapping("/{userId}/customers")
    public List<Customer> getUserCustomers(@PathVariable Long userId) {
        return this.userRepository.findById(userId).map(this.customerRepository::findByUser).get();
    }

    @DeleteMapping("/{userId}")
    public void deleteUser(@PathVariable Long userId) {
        this.userRepository.deleteById(userId);
    }

}

Kotlin

import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController


@RestController
@RequestMapping("/users")
class MyRestController(private val userRepository: UserRepository, private val customerRepository: CustomerRepository) {

    @GetMapping("/{userId}")
    fun getUser(@PathVariable userId: Long): User {
        return userRepository.findById(userId).get()
    }

    @GetMapping("/{userId}/customers")
    fun getUserCustomers(@PathVariable userId: Long): List<Customer> {
        return userRepository.findById(userId).map(customerRepository::findByUser).get()
    }

    @DeleteMapping("/{userId}")
    fun deleteUser(@PathVariable userId: Long) {
        userRepository.deleteById(userId)
    }

}

“WebMvc.fn”, the functional variant, separates the routing configuration from the actual handling of the requests, as shown in the following example:

Java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicate;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;

import static org.springframework.web.servlet.function.RequestPredicates.accept;
import static org.springframework.web.servlet.function.RouterFunctions.route;

@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {

    private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

    @Bean
    public RouterFunction<ServerResponse> routerFunction(MyUserHandler userHandler) {
        return route()
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                .build();
    }

}

Kotlin

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.servlet.function.RequestPredicates.accept
import org.springframework.web.servlet.function.RouterFunction
import org.springframework.web.servlet.function.RouterFunctions
import org.springframework.web.servlet.function.ServerResponse

@Configuration(proxyBeanMethods = false)
class MyRoutingConfiguration {

    @Bean
    fun routerFunction(userHandler: MyUserHandler): RouterFunction<ServerResponse> {
        return RouterFunctions.route()
            .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
            .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
            .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
            .build()
    }

    companion object {
        private val ACCEPT_JSON = accept(MediaType.APPLICATION_JSON)
    }

}

Java

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

@Component
public class MyUserHandler {

    public ServerResponse getUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

    public ServerResponse getUserCustomers(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

    public ServerResponse deleteUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

}

Kotlin

import org.springframework.stereotype.Component
import org.springframework.web.servlet.function.ServerRequest
import org.springframework.web.servlet.function.ServerResponse

@Component
class MyUserHandler {

    fun getUser(request: ServerRequest?): ServerResponse {
        return ServerResponse.ok().build()
    }

    fun getUserCustomers(request: ServerRequest?): ServerResponse {
        return ServerResponse.ok().build()
    }

    fun deleteUser(request: ServerRequest?): ServerResponse {
        return ServerResponse.ok().build()
    }

}

Spring MVC is part of the core Spring Framework, and detailed information is available in the reference documentation.
There are also several guides that cover Spring MVC available at spring.io/guides.

You can define as many RouterFunction beans as you like to modularize the definition of the router.
Beans can be ordered if you need to apply a precedence.

1.1.1. Spring MVC Auto-configuration

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
It replaces the need for @EnableWebMvc and the two cannot be used together.
In addition to Spring MVC’s defaults, the auto-configuration provides the following features:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.

  • Support for serving static resources, including support for WebJars (covered later in this document).

  • Automatic registration of Converter, GenericConverter, and Formatter beans.

  • Support for HttpMessageConverters (covered later in this document).

  • Automatic registration of MessageCodesResolver (covered later in this document).

  • Static index.html support.

  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

If you do not want to use the auto-configuration and want to take complete control of Spring MVC, add your own @Configuration annotated with @EnableWebMvc.
Alternatively, add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

1.1.2. Spring MVC Conversion Service

Spring MVC uses a different ConversionService to the one used to convert values from your application.properties or application.yaml file.
It means that Period, Duration and DataSize converters are not available and that @DurationUnit and @DataSizeUnit annotations will be ignored.

If you want to customize the ConversionService used by Spring MVC, you can provide a WebMvcConfigurer bean with an addFormatters method.
From this method you can register any converter that you like, or you can delegate to the static methods available on ApplicationConversionService.

Conversion can also be customized using the spring.mvc.format.* configuration properties.
When not configured, the following defaults are used:

Property DateTimeFormatter

spring.mvc.format.date

ofLocalizedDate(FormatStyle.SHORT)

spring.mvc.format.time

ofLocalizedTime(FormatStyle.SHORT)

spring.mvc.format.date-time

ofLocalizedDateTime(FormatStyle.SHORT)

1.1.3. HttpMessageConverters

Spring MVC uses the HttpMessageConverter interface to convert HTTP requests and responses.
Sensible defaults are included out of the box.
For example, objects can be automatically converted to JSON (by using the Jackson library) or XML (by using the Jackson XML extension, if available, or by using JAXB if the Jackson XML extension is not available).
By default, strings are encoded in UTF-8.

If you need to add or customize converters, you can use Spring Boot’s HttpMessageConverters class, as shown in the following listing:

Java

import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;

@Configuration(proxyBeanMethods = false)
public class MyHttpMessageConvertersConfiguration {

    @Bean
    public HttpMessageConverters customConverters() {
        HttpMessageConverter<?> additional = new AdditionalHttpMessageConverter();
        HttpMessageConverter<?> another = new AnotherHttpMessageConverter();
        return new HttpMessageConverters(additional, another);
    }

}

Kotlin

import org.springframework.boot.autoconfigure.http.HttpMessageConverters
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter

@Configuration(proxyBeanMethods = false)
class MyHttpMessageConvertersConfiguration {

    @Bean
    fun customConverters(): HttpMessageConverters {
        val additional: HttpMessageConverter<*> = AdditionalHttpMessageConverter()
        val another: HttpMessageConverter<*> = AnotherHttpMessageConverter()
        return HttpMessageConverters(additional, another)
    }

}

Any HttpMessageConverter bean that is present in the context is added to the list of converters.
You can also override default converters in the same way.

1.1.4. MessageCodesResolver

Spring MVC has a strategy for generating error codes for rendering error messages from binding errors: MessageCodesResolver.
If you set the spring.mvc.message-codes-resolver-format property PREFIX_ERROR_CODE or POSTFIX_ERROR_CODE, Spring Boot creates one for you (see the enumeration in DefaultMessageCodesResolver.Format).

1.1.5. Static Content

By default, Spring Boot serves static content from a directory called /static (or /public or /resources or /META-INF/resources) in the classpath or from the root of the ServletContext.
It uses the ResourceHttpRequestHandler from Spring MVC so that you can modify that behavior by adding your own WebMvcConfigurer and overriding the addResourceHandlers method.

In a stand-alone web application, the default servlet from the container is not enabled.
It can be enabled using the server.servlet.register-default-servlet property.

The default servlet acts as a fallback, serving content from the root of the ServletContext if Spring decides not to handle it.
Most of the time, this does not happen (unless you modify the default MVC configuration), because Spring can always handle requests through the DispatcherServlet.

By default, resources are mapped on /**, but you can tune that with the spring.mvc.static-path-pattern property.
For instance, relocating all resources to /resources/** can be achieved as follows:

Properties

spring.mvc.static-path-pattern=/resources/**

Yaml

spring:
  mvc:
    static-path-pattern: "/resources/**"

You can also customize the static resource locations by using the spring.web.resources.static-locations property (replacing the default values with a list of directory locations).
The root servlet context path, "/", is automatically added as a location as well.

In addition to the “standard” static resource locations mentioned earlier, a special case is made for Webjars content.
By default, any resources with a path in /webjars/** are served from jar files if they are packaged in the Webjars format.
The path can be customized with the spring.mvc.webjars-path-pattern property.

Do not use the src/main/webapp directory if your application is packaged as a jar.
Although this directory is a common standard, it works only with war packaging, and it is silently ignored by most build tools if you generate a jar.

Spring Boot also supports the advanced resource handling features provided by Spring MVC, allowing use cases such as cache-busting static resources or using version agnostic URLs for Webjars.

To use version agnostic URLs for Webjars, add the webjars-locator-core dependency.
Then declare your Webjar.
Using jQuery as an example, adding "/webjars/jquery/jquery.min.js" results in "/webjars/jquery/x.y.z/jquery.min.js" where x.y.z is the Webjar version.

If you use JBoss, you need to declare the webjars-locator-jboss-vfs dependency instead of the webjars-locator-core.
Otherwise, all Webjars resolve as a 404.

To use cache busting, the following configuration configures a cache busting solution for all static resources, effectively adding a content hash, such as <link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>, in URLs:

Properties

spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**

Yaml

spring:
  web:
    resources:
      chain:
        strategy:
          content:
            enabled: true
            paths: "/**"
Links to resources are rewritten in templates at runtime, thanks to a ResourceUrlEncodingFilter that is auto-configured for Thymeleaf and FreeMarker.
You should manually declare this filter when using JSPs.
Other template engines are currently not automatically supported but can be with custom template macros/helpers and the use of the ResourceUrlProvider.

When loading resources dynamically with, for example, a JavaScript module loader, renaming files is not an option.
That is why other strategies are also supported and can be combined.
A «fixed» strategy adds a static version string in the URL without changing the file name, as shown in the following example:

Properties

spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
spring.web.resources.chain.strategy.fixed.enabled=true
spring.web.resources.chain.strategy.fixed.paths=/js/lib/
spring.web.resources.chain.strategy.fixed.version=v12

Yaml

spring:
  web:
    resources:
      chain:
        strategy:
          content:
            enabled: true
            paths: "/**"
          fixed:
            enabled: true
            paths: "/js/lib/"
            version: "v12"

With this configuration, JavaScript modules located under "/js/lib/" use a fixed versioning strategy ("/v12/js/lib/mymodule.js"), while other resources still use the content one (<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>).

1.1.6. Welcome Page

Spring Boot supports both static and templated welcome pages.
It first looks for an index.html file in the configured static content locations.
If one is not found, it then looks for an index template.
If either is found, it is automatically used as the welcome page of the application.

1.1.7. Custom Favicon

As with other static resources, Spring Boot checks for a favicon.ico in the configured static content locations.
If such a file is present, it is automatically used as the favicon of the application.

1.1.8. Path Matching and Content Negotiation

Spring MVC can map incoming HTTP requests to handlers by looking at the request path and matching it to the mappings defined in your application (for example, @GetMapping annotations on Controller methods).

Spring Boot chooses to disable suffix pattern matching by default, which means that requests like "GET /projects/spring-boot.json" will not be matched to @GetMapping("/projects/spring-boot") mappings.
This is considered as a best practice for Spring MVC applications.
This feature was mainly useful in the past for HTTP clients which did not send proper «Accept» request headers; we needed to make sure to send the correct Content Type to the client.
Nowadays, Content Negotiation is much more reliable.

There are other ways to deal with HTTP clients that do not consistently send proper «Accept» request headers.
Instead of using suffix matching, we can use a query parameter to ensure that requests like "GET /projects/spring-boot?format=json" will be mapped to @GetMapping("/projects/spring-boot"):

Properties

spring.mvc.contentnegotiation.favor-parameter=true

Yaml

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true

Or if you prefer to use a different parameter name:

Properties

spring.mvc.contentnegotiation.favor-parameter=true
spring.mvc.contentnegotiation.parameter-name=myparam

Yaml

spring:
  mvc:
    contentnegotiation:
      favor-parameter: true
      parameter-name: "myparam"

Most standard media types are supported out-of-the-box, but you can also define new ones:

Properties

spring.mvc.contentnegotiation.media-types.markdown=text/markdown

Yaml

spring:
  mvc:
    contentnegotiation:
      media-types:
        markdown: "text/markdown"

As of Spring Framework 5.3, Spring MVC supports two strategies for matching request paths to controllers.
By default, Spring Boot uses the PathPatternParser strategy.
PathPatternParser is an optimized implementation but comes with some restrictions compared to the AntPathMatcher strategy.
PathPatternParser restricts usage of some path patterns variants.
It is also incompatible with configuring the DispatcherServlet with a path prefix (spring.mvc.servlet.path).

The strategy can be configured using the spring.mvc.pathmatch.matching-strategy configuration property, as shown in the following example:

Properties

spring.mvc.pathmatch.matching-strategy=ant-path-matcher

Yaml

spring:
  mvc:
    pathmatch:
      matching-strategy: "ant-path-matcher"

By default, Spring MVC will send a 404 Not Found error response if a handler is not found for a request.
To have a NoHandlerFoundException thrown instead, set configprop:spring.mvc.throw-exception-if-no-handler-found to true.
Note that, by default, the serving of static content is mapped to /** and will, therefore, provide a handler for all requests.
For a NoHandlerFoundException to be thrown, you must also set spring.mvc.static-path-pattern to a more specific value such as /resources/** or set spring.web.resources.add-mappings to false to disable serving of static content entirely.

1.1.9. ConfigurableWebBindingInitializer

Spring MVC uses a WebBindingInitializer to initialize a WebDataBinder for a particular request.
If you create your own ConfigurableWebBindingInitializer @Bean, Spring Boot automatically configures Spring MVC to use it.

1.1.10. Template Engines

As well as REST web services, you can also use Spring MVC to serve dynamic HTML content.
Spring MVC supports a variety of templating technologies, including Thymeleaf, FreeMarker, and JSPs.
Also, many other templating engines include their own Spring MVC integrations.

Spring Boot includes auto-configuration support for the following templating engines:

  • FreeMarker

  • Groovy

  • Thymeleaf

  • Mustache

If possible, JSPs should be avoided.
There are several known limitations when using them with embedded servlet containers.

When you use one of these templating engines with the default configuration, your templates are picked up automatically from src/main/resources/templates.

Depending on how you run your application, your IDE may order the classpath differently.
Running your application in the IDE from its main method results in a different ordering than when you run your application by using Maven or Gradle or from its packaged jar.
This can cause Spring Boot to fail to find the expected template.
If you have this problem, you can reorder the classpath in the IDE to place the module’s classes and resources first.

1.1.11. Error Handling

By default, Spring Boot provides an /error mapping that handles all errors in a sensible way, and it is registered as a “global” error page in the servlet container.
For machine clients, it produces a JSON response with details of the error, the HTTP status, and the exception message.
For browser clients, there is a “whitelabel” error view that renders the same data in HTML format (to customize it, add a View that resolves to error).

There are a number of server.error properties that can be set if you want to customize the default error handling behavior.
See the “Server Properties” section of the Appendix.

To replace the default behavior completely, you can implement ErrorController and register a bean definition of that type or add a bean of type ErrorAttributes to use the existing mechanism but replace the contents.

The BasicErrorController can be used as a base class for a custom ErrorController.
This is particularly useful if you want to add a handler for a new content type (the default is to handle text/html specifically and provide a fallback for everything else).
To do so, extend BasicErrorController, add a public method with a @RequestMapping that has a produces attribute, and create a bean of your new type.

As of Spring Framework 6.0, RFC 7807 Problem Details is supported.
Spring MVC can produce custom error messages with the application/problem+json media type, like:

{
  "type": "https://example.org/problems/unknown-project",
  "title": "Unknown project",
  "status": 404,
  "detail": "No project found for id 'spring-unknown'",
  "instance": "/projects/spring-unknown"
}

This support can be enabled by setting spring.mvc.problemdetails.enabled to true.

You can also define a class annotated with @ControllerAdvice to customize the JSON document to return for a particular controller and/or exception type, as shown in the following example:

Java

import jakarta.servlet.RequestDispatcher;
import jakarta.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 jakarta.servlet.RequestDispatcher
import jakarta.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])
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
    }

}

In the preceding example, if MyException is thrown by a controller defined in the same package as SomeController, a JSON representation of the MyErrorBody POJO is used instead of the ErrorAttributes representation.

In some cases, errors handled at the controller level are not recorded by the metrics infrastructure.
Applications can ensure that such exceptions are recorded with the request metrics by setting the handled exception as a request attribute:

Java

import jakarta.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 jakarta.servlet.http.HttpServletRequest
import org.springframework.boot.web.servlet.error.ErrorAttributes
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler

@Controller
class MyController {

    @ExceptionHandler(CustomException::class)
    fun handleCustomException(request: HttpServletRequest, ex: CustomException?): String {
        request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex)
        return "errorView"
    }

}
Custom Error Pages

If you want to display a custom HTML error page for a given status code, you can add a file to an /error directory.
Error pages can either be static HTML (that is, added under any of the static resource directories) or be built by using templates.
The name of the file should be the exact status code or a series mask.

For example, to map 404 to a static HTML file, your directory structure would be as follows:

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

To map all 5xx errors by using a FreeMarker template, your directory structure would be as follows:

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

For more complex mappings, you can also add beans that implement the ErrorViewResolver interface, as shown in the following example:

Java

import java.util.Map;

import jakarta.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) {
        // Use the request or status to optionally return a ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // We could add custom model values here
            new ModelAndView("myview");
        }
        return null;
    }

}

Kotlin

import jakarta.servlet.http.HttpServletRequest
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver
import org.springframework.http.HttpStatus
import org.springframework.web.servlet.ModelAndView

class MyErrorViewResolver : ErrorViewResolver {

    override fun resolveErrorView(request: HttpServletRequest, status: HttpStatus,
            model: Map<String, Any>): ModelAndView? {
        // Use the request or status to optionally return a ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // We could add custom model values here
            return ModelAndView("myview")
        }
        return null
    }

}
Mapping Error Pages Outside of Spring MVC

For applications that do not use Spring MVC, you can use the ErrorPageRegistrar interface to directly register ErrorPages.
This abstraction works directly with the underlying embedded servlet container and works even if you do not have a Spring MVC DispatcherServlet.

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"))
    }

}
If you register an ErrorPage with a path that ends up being handled by a Filter (as is common with some non-Spring web frameworks, like Jersey and Wicket), then the Filter has to be explicitly registered as an ERROR dispatcher, as shown in the following example:

Java

import java.util.EnumSet;

import jakarta.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 jakarta.servlet.DispatcherType
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.EnumSet

@Configuration(proxyBeanMethods = false)
class MyFilterConfiguration {

    @Bean
    fun myFilter(): FilterRegistrationBean<MyFilter> {
        val registration = FilterRegistrationBean(MyFilter())
        // ...
        registration.setDispatcherTypes(EnumSet.allOf(DispatcherType::class.java))
        return registration
    }

}

Note that the default FilterRegistrationBean does not include the ERROR dispatcher type.

Error Handling in a WAR Deployment

When deployed to a servlet container, Spring Boot uses its error page filter to forward a request with an error status to the appropriate error page.
This is necessary as the servlet specification does not provide an API for registering error pages.
Depending on the container that you are deploying your war file to and the technologies that your application uses, some additional configuration may be required.

The error page filter can only forward the request to the correct error page if the response has not already been committed.
By default, WebSphere Application Server 8.0 and later commits the response upon successful completion of a servlet’s service method.
You should disable this behavior by setting com.ibm.ws.webcontainer.invokeFlushAfterService to false.

1.1.12. CORS Support

Cross-origin resource sharing (CORS) is a W3C specification implemented by most browsers that lets you specify in a flexible way what kind of cross-domain requests are authorized, instead of using some less secure and less powerful approaches such as IFRAME or JSONP.

As of version 4.2, Spring MVC supports CORS.
Using controller method CORS configuration with @CrossOrigin annotations in your Spring Boot application does not require any specific configuration.
Global CORS configuration can be defined by registering a WebMvcConfigurer bean with a customized addCorsMappings(CorsRegistry) method, as shown in the following example:

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

}

1.2. JAX-RS and Jersey

If you prefer the JAX-RS programming model for REST endpoints, you can use one of the available implementations instead of Spring MVC.
Jersey and Apache CXF work quite well out of the box.
CXF requires you to register its Servlet or Filter as a @Bean in your application context.
Jersey has some native Spring support, so we also provide auto-configuration support for it in Spring Boot, together with a starter.

To get started with Jersey, include the spring-boot-starter-jersey as a dependency and then you need one @Bean of type ResourceConfig in which you register all the endpoints, as shown in the following example:

import org.glassfish.jersey.server.ResourceConfig;

import org.springframework.stereotype.Component;

@Component
public class MyJerseyConfig extends ResourceConfig {

    public MyJerseyConfig() {
        register(MyEndpoint.class);
    }

}
Jersey’s support for scanning executable archives is rather limited.
For example, it cannot scan for endpoints in a package found in a fully executable jar file or in WEB-INF/classes when running an executable war file.
To avoid this limitation, the packages method should not be used, and endpoints should be registered individually by using the register method, as shown in the preceding example.

For more advanced customizations, you can also register an arbitrary number of beans that implement ResourceConfigCustomizer.

All the registered endpoints should be @Components with HTTP resource annotations (@GET and others), as shown in the following example:

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.springframework.stereotype.Component;

@Component
@Path("/hello")
public class MyEndpoint {

    @GET
    public String message() {
        return "Hello";
    }

}

Since the Endpoint is a Spring @Component, its lifecycle is managed by Spring and you can use the @Autowired annotation to inject dependencies and use the @Value annotation to inject external configuration.
By default, the Jersey servlet is registered and mapped to /*.
You can change the mapping by adding @ApplicationPath to your ResourceConfig.

By default, Jersey is set up as a servlet in a @Bean of type ServletRegistrationBean named jerseyServletRegistration.
By default, the servlet is initialized lazily, but you can customize that behavior by setting spring.jersey.servlet.load-on-startup.
You can disable or override that bean by creating one of your own with the same name.
You can also use a filter instead of a servlet by setting spring.jersey.type=filter (in which case, the @Bean to replace or override is jerseyFilterRegistration).
The filter has an @Order, which you can set with spring.jersey.filter.order.
When using Jersey as a filter, a servlet that will handle any requests that are not intercepted by Jersey must be present.
If your application does not contain such a servlet, you may want to enable the default servlet by setting server.servlet.register-default-servlet to true.
Both the servlet and the filter registrations can be given init parameters by using spring.jersey.init.* to specify a map of properties.

1.3. Embedded Servlet Container Support

For servlet application, Spring Boot includes support for embedded Tomcat, Jetty, and Undertow servers.
Most developers use the appropriate “Starter” to obtain a fully configured instance.
By default, the embedded server listens for HTTP requests on port 8080.

1.3.1. Servlets, Filters, and Listeners

When using an embedded servlet container, you can register servlets, filters, and all the listeners (such as HttpSessionListener) from the servlet spec, either by using Spring beans or by scanning for servlet components.

Registering Servlets, Filters, and Listeners as Spring Beans

Any Servlet, Filter, or servlet *Listener instance that is a Spring bean is registered with the embedded container.
This can be particularly convenient if you want to refer to a value from your application.properties during configuration.

By default, if the context contains only a single Servlet, it is mapped to /.
In the case of multiple servlet beans, the bean name is used as a path prefix.
Filters map to /*.

If convention-based mapping is not flexible enough, you can use the ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean classes for complete control.

It is usually safe to leave filter beans unordered.
If a specific order is required, you should annotate the Filter with @Order or make it implement Ordered.
You cannot configure the order of a Filter by annotating its bean method with @Order.
If you cannot change the Filter class to add @Order or implement Ordered, you must define a FilterRegistrationBean for the Filter and set the registration bean’s order using the setOrder(int) method.
Avoid configuring a filter that reads the request body at Ordered.HIGHEST_PRECEDENCE, since it might go against the character encoding configuration of your application.
If a servlet filter wraps the request, it should be configured with an order that is less than or equal to OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER.

To see the order of every Filter in your application, enable debug level logging for the web logging group (logging.level.web=debug).
Details of the registered filters, including their order and URL patterns, will then be logged at startup.
Take care when registering Filter beans since they are initialized very early in the application lifecycle.
If you need to register a Filter that interacts with other beans, consider using a DelegatingFilterProxyRegistrationBean instead.

1.3.2. Servlet Context Initialization

Embedded servlet containers do not directly execute the jakarta.servlet.ServletContainerInitializer interface or Spring’s org.springframework.web.WebApplicationInitializer interface.
This is an intentional design decision intended to reduce the risk that third party libraries designed to run inside a war may break Spring Boot applications.

If you need to perform servlet context initialization in a Spring Boot application, you should register a bean that implements the org.springframework.boot.web.servlet.ServletContextInitializer interface.
The single onStartup method provides access to the ServletContext and, if necessary, can easily be used as an adapter to an existing WebApplicationInitializer.

Scanning for Servlets, Filters, and listeners

When using an embedded container, automatic registration of classes annotated with @WebServlet, @WebFilter, and @WebListener can be enabled by using @ServletComponentScan.

@ServletComponentScan has no effect in a standalone container, where the container’s built-in discovery mechanisms are used instead.

1.3.3. The ServletWebServerApplicationContext

Under the hood, Spring Boot uses a different type of ApplicationContext for embedded servlet container support.
The ServletWebServerApplicationContext is a special type of WebApplicationContext that bootstraps itself by searching for a single ServletWebServerFactory bean.
Usually a TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory has been auto-configured.

You usually do not need to be aware of these implementation classes.
Most applications are auto-configured, and the appropriate ApplicationContext and ServletWebServerFactory are created on your behalf.

In an embedded container setup, the ServletContext is set as part of server startup which happens during application context initialization.
Because of this beans in the ApplicationContext cannot be reliably initialized with a ServletContext.
One way to get around this is to inject ApplicationContext as a dependency of the bean and access the ServletContext only when it is needed.
Another way is to use a callback once the server has started.
This can be done using an ApplicationListener which listens for the ApplicationStartedEvent as follows:

import jakarta.servlet.ServletContext;

import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.web.context.WebApplicationContext;

public class MyDemoBean implements ApplicationListener<ApplicationStartedEvent> {

    private ServletContext servletContext;

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        ApplicationContext applicationContext = event.getApplicationContext();
        this.servletContext = ((WebApplicationContext) applicationContext).getServletContext();
    }

}

1.3.4. Customizing Embedded Servlet Containers

Common servlet container settings can be configured by using Spring Environment properties.
Usually, you would define the properties in your application.properties or application.yaml file.

Common server settings include:

  • Network settings: Listen port for incoming HTTP requests (server.port), interface address to bind to server.address, and so on.

  • Session settings: Whether the session is persistent (server.servlet.session.persistent), session timeout (server.servlet.session.timeout), location of session data (server.servlet.session.store-dir), and session-cookie configuration (server.servlet.session.cookie.*).

  • Error management: Location of the error page (server.error.path) and so on.

  • SSL

  • HTTP compression

Spring Boot tries as much as possible to expose common settings, but this is not always possible.
For those cases, dedicated namespaces offer server-specific customizations (see server.tomcat and server.undertow).
For instance, access logs can be configured with specific features of the embedded servlet container.

SameSite Cookies

The SameSite cookie attribute can be used by web browsers to control if and how cookies are submitted in cross-site requests.
The attribute is particularly relevant for modern web browsers which have started to change the default value that is used when the attribute is missing.

If you want to change the SameSite attribute of your session cookie, you can use the server.servlet.session.cookie.same-site property.
This property is supported by auto-configured Tomcat, Jetty and Undertow servers.
It is also used to configure Spring Session servlet based SessionRepository beans.

For example, if you want your session cookie to have a SameSite attribute of None, you can add the following to your application.properties or application.yaml file:

Properties

server.servlet.session.cookie.same-site=none

Yaml

server:
  servlet:
    session:
      cookie:
        same-site: "none"

If you want to change the SameSite attribute on other cookies added to your HttpServletResponse, you can use a CookieSameSiteSupplier.
The CookieSameSiteSupplier is passed a Cookie and may return a SameSite value, or null.

There are a number of convenience factory and filter methods that you can use to quickly match specific cookies.
For example, adding the following bean will automatically apply a SameSite of Lax for all cookies with a name that matches the regular expression myapp.*.

Java

import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MySameSiteConfiguration {

    @Bean
    public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
        return CookieSameSiteSupplier.ofLax().whenHasNameMatching("myapp.*");
    }

}

Kotlin

import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
class MySameSiteConfiguration {

    @Bean
    fun applicationCookieSameSiteSupplier(): CookieSameSiteSupplier {
        return CookieSameSiteSupplier.ofLax().whenHasNameMatching("myapp.*")
    }

}
Programmatic Customization

If you need to programmatically configure your embedded servlet container, you can register a Spring bean that implements the WebServerFactoryCustomizer interface.
WebServerFactoryCustomizer provides access to the ConfigurableServletWebServerFactory, which includes numerous customization setter methods.
The following example shows programmatically setting the port:

Java

import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    @Override
    public void customize(ConfigurableServletWebServerFactory server) {
        server.setPort(9000);
    }

}

Kotlin

import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory
import org.springframework.stereotype.Component

@Component
class MyWebServerFactoryCustomizer : WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    override fun customize(server: ConfigurableServletWebServerFactory) {
        server.setPort(9000)
    }

}

TomcatServletWebServerFactory, JettyServletWebServerFactory and UndertowServletWebServerFactory are dedicated variants of ConfigurableServletWebServerFactory that have additional customization setter methods for Tomcat, Jetty and Undertow respectively.
The following example shows how to customize TomcatServletWebServerFactory that provides access to Tomcat-specific configuration options:

Java

import java.time.Duration;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class MyTomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory server) {
        server.addConnectorCustomizers((connector) -> connector.setAsyncTimeout(Duration.ofSeconds(20).toMillis()));
    }

}

Kotlin

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.stereotype.Component
import java.time.Duration

@Component
class MyTomcatWebServerFactoryCustomizer : WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    override fun customize(server: TomcatServletWebServerFactory) {
        server.addConnectorCustomizers({ connector -> connector.asyncTimeout = Duration.ofSeconds(20).toMillis() })
    }

}
Customizing ConfigurableServletWebServerFactory Directly

For more advanced use cases that require you to extend from ServletWebServerFactory, you can expose a bean of such type yourself.

Setters are provided for many configuration options.
Several protected method “hooks” are also provided should you need to do something more exotic.
See the source code documentation for details.

Auto-configured customizers are still applied on your custom factory, so use that option carefully.

1.3.5. JSP Limitations

When running a Spring Boot application that uses an embedded servlet container (and is packaged as an executable archive), there are some limitations in the JSP support.

  • With Jetty and Tomcat, it should work if you use war packaging.
    An executable war will work when launched with java -jar, and will also be deployable to any standard container.
    JSPs are not supported when using an executable jar.

  • Undertow does not support JSPs.

  • Creating a custom error.jsp page does not override the default view for error handling.
    Custom error pages should be used instead.

2. Reactive Web Applications

Spring Boot simplifies development of reactive web applications by providing auto-configuration for Spring Webflux.

2.1. The “Spring WebFlux Framework”

Spring WebFlux is the new reactive web framework introduced in Spring Framework 5.0.
Unlike Spring MVC, it does not require the servlet API, is fully asynchronous and non-blocking, and implements the Reactive Streams specification through the Reactor project.

Spring WebFlux comes in two flavors: functional and annotation-based.
The annotation-based one is quite close to the Spring MVC model, as shown in the following example:

Java

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class MyRestController {

    private final UserRepository userRepository;

    private final CustomerRepository customerRepository;

    public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {
        this.userRepository = userRepository;
        this.customerRepository = customerRepository;
    }

    @GetMapping("/{userId}")
    public Mono<User> getUser(@PathVariable Long userId) {
        return this.userRepository.findById(userId);
    }

    @GetMapping("/{userId}/customers")
    public Flux<Customer> getUserCustomers(@PathVariable Long userId) {
        return this.userRepository.findById(userId).flatMapMany(this.customerRepository::findByUser);
    }

    @DeleteMapping("/{userId}")
    public Mono<Void> deleteUser(@PathVariable Long userId) {
        return this.userRepository.deleteById(userId);
    }

}

Kotlin

import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

@RestController
@RequestMapping("/users")
class MyRestController(private val userRepository: UserRepository, private val customerRepository: CustomerRepository) {

    @GetMapping("/{userId}")
    fun getUser(@PathVariable userId: Long): Mono<User?> {
        return userRepository.findById(userId)
    }

    @GetMapping("/{userId}/customers")
    fun getUserCustomers(@PathVariable userId: Long): Flux<Customer> {
        return userRepository.findById(userId).flatMapMany { user: User? ->
            customerRepository.findByUser(user)
        }
    }

    @DeleteMapping("/{userId}")
    fun deleteUser(@PathVariable userId: Long): Mono<Void> {
        return userRepository.deleteById(userId)
    }

}

“WebFlux.fn”, the functional variant, separates the routing configuration from the actual handling of the requests, as shown in the following example:

Java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {

    private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

    @Bean
    public RouterFunction<ServerResponse> monoRouterFunction(MyUserHandler userHandler) {
        return route()
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                .build();
    }

}

Kotlin

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.RequestPredicates.DELETE
import org.springframework.web.reactive.function.server.RequestPredicates.GET
import org.springframework.web.reactive.function.server.RequestPredicates.accept
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions
import org.springframework.web.reactive.function.server.ServerResponse

@Configuration(proxyBeanMethods = false)
class MyRoutingConfiguration {

    @Bean
    fun monoRouterFunction(userHandler: MyUserHandler): RouterFunction<ServerResponse> {
        return RouterFunctions.route(
            GET("/{user}").and(ACCEPT_JSON), userHandler::getUser).andRoute(
            GET("/{user}/customers").and(ACCEPT_JSON), userHandler::getUserCustomers).andRoute(
            DELETE("/{user}").and(ACCEPT_JSON), userHandler::deleteUser)
    }

    companion object {
        private val ACCEPT_JSON = accept(MediaType.APPLICATION_JSON)
    }

}

Java

import reactor.core.publisher.Mono;

import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

@Component
public class MyUserHandler {

    public Mono<ServerResponse> getUser(ServerRequest request) {
        ...
    }

    public Mono<ServerResponse> getUserCustomers(ServerRequest request) {
        ...
    }

    public Mono<ServerResponse> deleteUser(ServerRequest request) {
        ...
    }

}

Kotlin

import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono

@Component
class MyUserHandler {

    fun getUser(request: ServerRequest?): Mono<ServerResponse> {
        return ServerResponse.ok().build()
    }

    fun getUserCustomers(request: ServerRequest?): Mono<ServerResponse> {
        return ServerResponse.ok().build()
    }

    fun deleteUser(request: ServerRequest?): Mono<ServerResponse> {
        return ServerResponse.ok().build()
    }

}

WebFlux is part of the Spring Framework and detailed information is available in its reference documentation.

You can define as many RouterFunction beans as you like to modularize the definition of the router.
Beans can be ordered if you need to apply a precedence.

To get started, add the spring-boot-starter-webflux module to your application.

Adding both spring-boot-starter-web and spring-boot-starter-webflux modules in your application results in Spring Boot auto-configuring Spring MVC, not WebFlux.
This behavior has been chosen because many Spring developers add spring-boot-starter-webflux to their Spring MVC application to use the reactive WebClient.
You can still enforce your choice by setting the chosen application type to SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE).

2.1.1. Spring WebFlux Auto-configuration

Spring Boot provides auto-configuration for Spring WebFlux that works well with most applications.

The auto-configuration adds the following features on top of Spring’s defaults:

  • Configuring codecs for HttpMessageReader and HttpMessageWriter instances (described later in this document).

  • Support for serving static resources, including support for WebJars (described later in this document).

If you want to keep Spring Boot WebFlux features and you want to add additional WebFlux configuration, you can add your own @Configuration class of type WebFluxConfigurer but without @EnableWebFlux.

If you want to take complete control of Spring WebFlux, you can add your own @Configuration annotated with @EnableWebFlux.

2.1.2. Spring WebFlux Conversion Service

If you want to customize the ConversionService used by Spring WebFlux, you can provide a WebFluxConfigurer bean with an addFormatters method.

Conversion can also be customized using the spring.webflux.format.* configuration properties.
When not configured, the following defaults are used:

Property DateTimeFormatter

spring.webflux.format.date

ofLocalizedDate(FormatStyle.SHORT)

spring.webflux.format.time

ofLocalizedTime(FormatStyle.SHORT)

spring.webflux.format.date-time

ofLocalizedDateTime(FormatStyle.SHORT)

2.1.3. HTTP Codecs with HttpMessageReaders and HttpMessageWriters

Spring WebFlux uses the HttpMessageReader and HttpMessageWriter interfaces to convert HTTP requests and responses.
They are configured with CodecConfigurer to have sensible defaults by looking at the libraries available in your classpath.

Spring Boot provides dedicated configuration properties for codecs, spring.codec.*.
It also applies further customization by using CodecCustomizer instances.
For example, spring.jackson.* configuration keys are applied to the Jackson codec.

If you need to add or customize codecs, you can create a custom CodecCustomizer component, as shown in the following example:

Java

import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerSentEventHttpMessageReader;

@Configuration(proxyBeanMethods = false)
public class MyCodecsConfiguration {

    @Bean
    public CodecCustomizer myCodecCustomizer() {
        return (configurer) -> {
            configurer.registerDefaults(false);
            configurer.customCodecs().register(new ServerSentEventHttpMessageReader());
            // ...
        };
    }

}

Kotlin

import org.springframework.boot.web.codec.CodecCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.http.codec.CodecConfigurer
import org.springframework.http.codec.ServerSentEventHttpMessageReader

class MyCodecsConfiguration {

    @Bean
    fun myCodecCustomizer(): CodecCustomizer {
        return CodecCustomizer { configurer: CodecConfigurer ->
            configurer.registerDefaults(false)
            configurer.customCodecs().register(ServerSentEventHttpMessageReader())
        }
    }

}

2.1.4. Static Content

By default, Spring Boot serves static content from a directory called /static (or /public or /resources or /META-INF/resources) in the classpath.
It uses the ResourceWebHandler from Spring WebFlux so that you can modify that behavior by adding your own WebFluxConfigurer and overriding the addResourceHandlers method.

By default, resources are mapped on /**, but you can tune that by setting the spring.webflux.static-path-pattern property.
For instance, relocating all resources to /resources/** can be achieved as follows:

Properties

spring.webflux.static-path-pattern=/resources/**

Yaml

spring:
  webflux:
    static-path-pattern: "/resources/**"

You can also customize the static resource locations by using spring.web.resources.static-locations.
Doing so replaces the default values with a list of directory locations.
If you do so, the default welcome page detection switches to your custom locations.
So, if there is an index.html in any of your locations on startup, it is the home page of the application.

In addition to the “standard” static resource locations listed earlier, a special case is made for Webjars content.
By default, any resources with a path in /webjars/** are served from jar files if they are packaged in the Webjars format.
The path can be customized with the spring.webflux.webjars-path-pattern property.

Spring WebFlux applications do not strictly depend on the servlet API, so they cannot be deployed as war files and do not use the src/main/webapp directory.

2.1.5. Welcome Page

Spring Boot supports both static and templated welcome pages.
It first looks for an index.html file in the configured static content locations.
If one is not found, it then looks for an index template.
If either is found, it is automatically used as the welcome page of the application.

2.1.6. Template Engines

As well as REST web services, you can also use Spring WebFlux to serve dynamic HTML content.
Spring WebFlux supports a variety of templating technologies, including Thymeleaf, FreeMarker, and Mustache.

Spring Boot includes auto-configuration support for the following templating engines:

  • FreeMarker

  • Thymeleaf

  • Mustache

When you use one of these templating engines with the default configuration, your templates are picked up automatically from src/main/resources/templates.

2.1.7. Error Handling

Spring Boot provides a WebExceptionHandler that handles all errors in a sensible way.
Its position in the processing order is immediately before the handlers provided by WebFlux, which are considered last.
For machine clients, it produces a JSON response with details of the error, the HTTP status, and the exception message.
For browser clients, there is a “whitelabel” error handler that renders the same data in HTML format.
You can also provide your own HTML templates to display errors (see the next section).

Before customizing error handling in Spring Boot directly, you can leverage the RFC 7807 Problem Details support in Spring WebFlux.
Spring WebFlux can produce custom error messages with the application/problem+json media type, like:

{
  "type": "https://example.org/problems/unknown-project",
  "title": "Unknown project",
  "status": 404,
  "detail": "No project found for id 'spring-unknown'",
  "instance": "/projects/spring-unknown"
}

This support can be enabled by setting spring.webflux.problemdetails.enabled to true.

The first step to customizing this feature often involves using the existing mechanism but replacing or augmenting the error contents.
For that, you can add a bean of type ErrorAttributes.

To change the error handling behavior, you can implement ErrorWebExceptionHandler and register a bean definition of that type.
Because an ErrorWebExceptionHandler is quite low-level, Spring Boot also provides a convenient AbstractErrorWebExceptionHandler to let you handle errors in a WebFlux functional way, as shown in the following example:

Java

import reactor.core.publisher.Mono;

import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder;

@Component
public class MyErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {

    public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources,
            ApplicationContext applicationContext) {
        super(errorAttributes, resources, applicationContext);
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml);
    }

    private boolean acceptsXml(ServerRequest request) {
        return request.headers().accept().contains(MediaType.APPLICATION_XML);
    }

    public Mono<ServerResponse> handleErrorAsXml(ServerRequest request) {
        BodyBuilder builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR);
        // ... additional builder calls
        return builder.build();
    }

}

Kotlin

import org.springframework.boot.autoconfigure.web.WebProperties
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler
import org.springframework.boot.web.reactive.error.ErrorAttributes
import org.springframework.context.ApplicationContext
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono

@Component
class MyErrorWebExceptionHandler(errorAttributes: ErrorAttributes?, resources: WebProperties.Resources?,
    applicationContext: ApplicationContext?) : AbstractErrorWebExceptionHandler(errorAttributes, resources, applicationContext) {

    override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> {
        return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml)
    }

    private fun acceptsXml(request: ServerRequest): Boolean {
        return request.headers().accept().contains(MediaType.APPLICATION_XML)
    }

    fun handleErrorAsXml(request: ServerRequest?): Mono<ServerResponse> {
        val builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
        // ... additional builder calls
        return builder.build()
    }

}

For a more complete picture, you can also subclass DefaultErrorWebExceptionHandler directly and override specific methods.

In some cases, errors handled at the controller or handler function level are not recorded by the metrics infrastructure.
Applications can ensure that such exceptions are recorded with the request metrics by setting the handled exception as a request attribute:

Java

import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.result.view.Rendering;
import org.springframework.web.server.ServerWebExchange;

@Controller
public class MyExceptionHandlingController {

    @GetMapping("/profile")
    public Rendering userProfile() {
        // ...
        throw new IllegalStateException();
    }

    @ExceptionHandler(IllegalStateException.class)
    public Rendering handleIllegalState(ServerWebExchange exchange, IllegalStateException exc) {
        exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc);
        return Rendering.view("errorView").modelAttribute("message", exc.getMessage()).build();
    }

}

Kotlin

import org.springframework.boot.web.reactive.error.ErrorAttributes
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.reactive.result.view.Rendering
import org.springframework.web.server.ServerWebExchange

@Controller
class MyExceptionHandlingController {

    @GetMapping("/profile")
    fun userProfile(): Rendering {
        // ...
        throw IllegalStateException()
    }

    @ExceptionHandler(IllegalStateException::class)
    fun handleIllegalState(exchange: ServerWebExchange, exc: IllegalStateException): Rendering {
        exchange.attributes.putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc)
        return Rendering.view("errorView").modelAttribute("message", exc.message ?: "").build()
    }

}
Custom Error Pages

If you want to display a custom HTML error page for a given status code, you can add views that resolve from error/*, for example by adding files to a /error directory.
Error pages can either be static HTML (that is, added under any of the static resource directories) or built with templates.
The name of the file should be the exact status code, a status code series mask, or error for a default if nothing else matches.
Note that the path to the default error view is error/error, whereas with Spring MVC the default error view is error.

For example, to map 404 to a static HTML file, your directory structure would be as follows:

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

To map all 5xx errors by using a Mustache template, your directory structure would be as follows:

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

2.1.8. Web Filters

Spring WebFlux provides a WebFilter interface that can be implemented to filter HTTP request-response exchanges.
WebFilter beans found in the application context will be automatically used to filter each exchange.

Where the order of the filters is important they can implement Ordered or be annotated with @Order.
Spring Boot auto-configuration may configure web filters for you.
When it does so, the orders shown in the following table will be used:

Web Filter Order

ServerHttpObservationFilter (Micrometer Observability)

Ordered.HIGHEST_PRECEDENCE + 1

WebFilterChainProxy (Spring Security)

-100

HttpExchangesWebFilter

Ordered.LOWEST_PRECEDENCE - 10

2.2. Embedded Reactive Server Support

Spring Boot includes support for the following embedded reactive web servers: Reactor Netty, Tomcat, Jetty, and Undertow.
Most developers use the appropriate “Starter” to obtain a fully configured instance.
By default, the embedded server listens for HTTP requests on port 8080.

2.3. Reactive Server Resources Configuration

When auto-configuring a Reactor Netty or Jetty server, Spring Boot will create specific beans that will provide HTTP resources to the server instance: ReactorResourceFactory or JettyResourceFactory.

By default, those resources will be also shared with the Reactor Netty and Jetty clients for optimal performances, given:

  • the same technology is used for server and client

  • the client instance is built using the WebClient.Builder bean auto-configured by Spring Boot

Developers can override the resource configuration for Jetty and Reactor Netty by providing a custom ReactorResourceFactory or JettyResourceFactory bean — this will be applied to both clients and servers.

You can learn more about the resource configuration on the client side in the WebClient Runtime section.

3. Graceful Shutdown

Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and servlet-based web applications.
It occurs as part of closing the application context and is performed in the earliest phase of stopping SmartLifecycle beans.
This stop processing uses a timeout which provides a grace period during which existing requests will be allowed to complete but no new requests will be permitted.
The exact way in which new requests are not permitted varies depending on the web server that is being used.
Jetty, Reactor Netty, and Tomcat will stop accepting requests at the network layer.
Undertow will accept requests but respond immediately with a service unavailable (503) response.

Graceful shutdown with Tomcat requires Tomcat 9.0.33 or later.

To enable graceful shutdown, configure the server.shutdown property, as shown in the following example:

Yaml

server:
  shutdown: "graceful"

To configure the timeout period, configure the spring.lifecycle.timeout-per-shutdown-phase property, as shown in the following example:

Properties

spring.lifecycle.timeout-per-shutdown-phase=20s

Yaml

spring:
  lifecycle:
    timeout-per-shutdown-phase: "20s"
Using graceful shutdown with your IDE may not work properly if it does not send a proper SIGTERM signal.
See the documentation of your IDE for more details.

4. Spring Security

If Spring Security is on the classpath, then web applications are secured by default.
Spring Boot relies on Spring Security’s content-negotiation strategy to determine whether to use httpBasic or formLogin.
To add method-level security to a web application, you can also add @EnableGlobalMethodSecurity with your desired settings.
Additional information can be found in the Spring Security Reference Guide.

The default UserDetailsService has a single user.
The user name is user, and the password is random and is printed at WARN level when the application starts, as shown in the following example:

Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35

This generated password is for development use only. Your security configuration must be updated before running your application in production.
If you fine-tune your logging configuration, ensure that the org.springframework.boot.autoconfigure.security category is set to log WARN-level messages.
Otherwise, the default password is not printed.

You can change the username and password by providing a spring.security.user.name and spring.security.user.password.

The basic features you get by default in a web application are:

  • A UserDetailsService (or ReactiveUserDetailsService in case of a WebFlux application) bean with in-memory store and a single user with a generated password (see SecurityProperties.User for the properties of the user).

  • Form-based login or HTTP Basic security (depending on the Accept header in the request) for the entire application (including actuator endpoints if actuator is on the classpath).

  • A DefaultAuthenticationEventPublisher for publishing authentication events.

You can provide a different AuthenticationEventPublisher by adding a bean for it.

4.1. MVC Security

The default security configuration is implemented in SecurityAutoConfiguration and UserDetailsServiceAutoConfiguration.
SecurityAutoConfiguration imports SpringBootWebSecurityConfiguration for web security and UserDetailsServiceAutoConfiguration configures authentication, which is also relevant in non-web applications.
To switch off the default web application security configuration completely or to combine multiple Spring Security components such as OAuth2 Client and Resource Server, add a bean of type SecurityFilterChain (doing so does not disable the UserDetailsService configuration or Actuator’s security).

To also switch off the UserDetailsService configuration, you can add a bean of type UserDetailsService, AuthenticationProvider, or AuthenticationManager.

Access rules can be overridden by adding a custom SecurityFilterChain bean.
Spring Boot provides convenience methods that can be used to override access rules for actuator endpoints and static resources.
EndpointRequest can be used to create a RequestMatcher that is based on the management.endpoints.web.base-path property.
PathRequest can be used to create a RequestMatcher for resources in commonly used locations.

4.2. WebFlux Security

Similar to Spring MVC applications, you can secure your WebFlux applications by adding the spring-boot-starter-security dependency.
The default security configuration is implemented in ReactiveSecurityAutoConfiguration and UserDetailsServiceAutoConfiguration.
ReactiveSecurityAutoConfiguration imports WebFluxSecurityConfiguration for web security and UserDetailsServiceAutoConfiguration configures authentication, which is also relevant in non-web applications.
To switch off the default web application security configuration completely, you can add a bean of type WebFilterChainProxy (doing so does not disable the UserDetailsService configuration or Actuator’s security).

To also switch off the UserDetailsService configuration, you can add a bean of type ReactiveUserDetailsService or ReactiveAuthenticationManager.

Access rules and the use of multiple Spring Security components such as OAuth 2 Client and Resource Server can be configured by adding a custom SecurityWebFilterChain bean.
Spring Boot provides convenience methods that can be used to override access rules for actuator endpoints and static resources.
EndpointRequest can be used to create a ServerWebExchangeMatcher that is based on the management.endpoints.web.base-path property.

PathRequest can be used to create a ServerWebExchangeMatcher for resources in commonly used locations.

For example, you can customize your security configuration by adding something like:

Java

import org.springframework.boot.autoconfigure.security.reactive.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration(proxyBeanMethods = false)
public class MyWebFluxSecurityConfiguration {

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.authorizeExchange((exchange) -> {
            exchange.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
            exchange.pathMatchers("/foo", "/bar").authenticated();
        });
        http.formLogin(withDefaults());
        return http.build();
    }

}

Kotlin

import org.springframework.boot.autoconfigure.security.reactive.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.Customizer
import org.springframework.security.config.Customizer.withDefaults
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain

@Configuration(proxyBeanMethods = false)
class MyWebFluxSecurityConfiguration {

    @Bean
    fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http.authorizeExchange { spec ->
            spec.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
            spec.pathMatchers("/foo", "/bar").authenticated()
        }
        http.formLogin(withDefaults())
        return http.build()
    }

}

4.3. OAuth2

OAuth2 is a widely used authorization framework that is supported by Spring.

4.3.1. Client

If you have spring-security-oauth2-client on your classpath, you can take advantage of some auto-configuration to set up OAuth2/Open ID Connect clients.
This configuration makes use of the properties under OAuth2ClientProperties.
The same properties are applicable to both servlet and reactive applications.

You can register multiple OAuth2 clients and providers under the spring.security.oauth2.client prefix, as shown in the following example:

Properties

spring.security.oauth2.client.registration.my-login-client.client-id=abcd
spring.security.oauth2.client.registration.my-login-client.client-secret=password
spring.security.oauth2.client.registration.my-login-client.client-name=Client for OpenID Connect
spring.security.oauth2.client.registration.my-login-client.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-login-client.scope=openid,profile,email,phone,address
spring.security.oauth2.client.registration.my-login-client.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.my-login-client.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.my-login-client.authorization-grant-type=authorization_code

spring.security.oauth2.client.registration.my-client-1.client-id=abcd
spring.security.oauth2.client.registration.my-client-1.client-secret=password
spring.security.oauth2.client.registration.my-client-1.client-name=Client for user scope
spring.security.oauth2.client.registration.my-client-1.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-client-1.scope=user
spring.security.oauth2.client.registration.my-client-1.redirect-uri={baseUrl}/authorized/user
spring.security.oauth2.client.registration.my-client-1.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.my-client-1.authorization-grant-type=authorization_code

spring.security.oauth2.client.registration.my-client-2.client-id=abcd
spring.security.oauth2.client.registration.my-client-2.client-secret=password
spring.security.oauth2.client.registration.my-client-2.client-name=Client for email scope
spring.security.oauth2.client.registration.my-client-2.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-client-2.scope=email
spring.security.oauth2.client.registration.my-client-2.redirect-uri={baseUrl}/authorized/email
spring.security.oauth2.client.registration.my-client-2.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.my-client-2.authorization-grant-type=authorization_code

spring.security.oauth2.client.provider.my-oauth-provider.authorization-uri=https://my-auth-server.com/oauth2/authorize
spring.security.oauth2.client.provider.my-oauth-provider.token-uri=https://my-auth-server.com/oauth2/token
spring.security.oauth2.client.provider.my-oauth-provider.user-info-uri=https://my-auth-server.com/userinfo
spring.security.oauth2.client.provider.my-oauth-provider.user-info-authentication-method=header
spring.security.oauth2.client.provider.my-oauth-provider.jwk-set-uri=https://my-auth-server.com/oauth2/jwks
spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=name

Yaml

spring:
  security:
    oauth2:
      client:
        registration:
          my-login-client:
            client-id: "abcd"
            client-secret: "password"
            client-name: "Client for OpenID Connect"
            provider: "my-oauth-provider"
            scope: "openid,profile,email,phone,address"
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            client-authentication-method: "client_secret_basic"
            authorization-grant-type: "authorization_code"

          my-client-1:
            client-id: "abcd"
            client-secret: "password"
            client-name: "Client for user scope"
            provider: "my-oauth-provider"
            scope: "user"
            redirect-uri: "{baseUrl}/authorized/user"
            client-authentication-method: "client_secret_basic"
            authorization-grant-type: "authorization_code"

          my-client-2:
            client-id: "abcd"
            client-secret: "password"
            client-name: "Client for email scope"
            provider: "my-oauth-provider"
            scope: "email"
            redirect-uri: "{baseUrl}/authorized/email"
            client-authentication-method: "client_secret_basic"
            authorization-grant-type: "authorization_code"

        provider:
          my-oauth-provider:
            authorization-uri: "https://my-auth-server.com/oauth2/authorize"
            token-uri: "https://my-auth-server.com/oauth2/token"
            user-info-uri: "https://my-auth-server.com/userinfo"
            user-info-authentication-method: "header"
            jwk-set-uri: "https://my-auth-server.com/oauth2/jwks"
            user-name-attribute: "name"

For OpenID Connect providers that support OpenID Connect discovery, the configuration can be further simplified.
The provider needs to be configured with an issuer-uri which is the URI that it asserts as its Issuer Identifier.
For example, if the issuer-uri provided is «https://example.com», then an «OpenID Provider Configuration Request» will be made to «https://example.com/.well-known/openid-configuration».
The result is expected to be an «OpenID Provider Configuration Response».
The following example shows how an OpenID Connect Provider can be configured with the issuer-uri:

Properties

spring.security.oauth2.client.provider.oidc-provider.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/

Yaml

spring:
  security:
    oauth2:
      client:
        provider:
          oidc-provider:
            issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"

By default, Spring Security’s OAuth2LoginAuthenticationFilter only processes URLs matching /login/oauth2/code/*.
If you want to customize the redirect-uri to use a different pattern, you need to provide configuration to process that custom pattern.
For example, for servlet applications, you can add your own SecurityFilterChain that resembles the following:

Java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class MyOAuthClientConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((requests) -> requests
                .anyRequest().authenticated()
            )
            .oauth2Login((login) -> login
                .redirectionEndpoint((endpoint) -> endpoint
                    .baseUri("/login/oauth2/callback/*")
                )
            );
        return http.build();
    }

}

Kotlin

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
open class MyOAuthClientConfiguration {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2Login {
                redirectionEndpoint {
                    baseUri = "/login/oauth2/callback/*"
                }
            }
        }
        return http.build()
    }

}
Spring Boot auto-configures an InMemoryOAuth2AuthorizedClientService which is used by Spring Security for the management of client registrations.
The InMemoryOAuth2AuthorizedClientService has limited capabilities and we recommend using it only for development environments.
For production environments, consider using a JdbcOAuth2AuthorizedClientService or creating your own implementation of OAuth2AuthorizedClientService.
OAuth2 Client Registration for Common Providers

For common OAuth2 and OpenID providers, including Google, Github, Facebook, and Okta, we provide a set of provider defaults (google, github, facebook, and okta, respectively).

If you do not need to customize these providers, you can set the provider attribute to the one for which you need to infer defaults.
Also, if the key for the client registration matches a default supported provider, Spring Boot infers that as well.

In other words, the two configurations in the following example use the Google provider:

Properties

spring.security.oauth2.client.registration.my-client.client-id=abcd
spring.security.oauth2.client.registration.my-client.client-secret=password
spring.security.oauth2.client.registration.my-client.provider=google
spring.security.oauth2.client.registration.google.client-id=abcd
spring.security.oauth2.client.registration.google.client-secret=password

Yaml

spring:
  security:
    oauth2:
      client:
        registration:
          my-client:
            client-id: "abcd"
            client-secret: "password"
            provider: "google"
          google:
            client-id: "abcd"
            client-secret: "password"

4.3.2. Resource Server

If you have spring-security-oauth2-resource-server on your classpath, Spring Boot can set up an OAuth2 Resource Server.
For JWT configuration, a JWK Set URI or OIDC Issuer URI needs to be specified, as shown in the following examples:

Properties

spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://example.com/oauth2/default/v1/keys

Yaml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: "https://example.com/oauth2/default/v1/keys"

Properties

spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/

Yaml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"
If the authorization server does not support a JWK Set URI, you can configure the resource server with the Public Key used for verifying the signature of the JWT.
This can be done using the spring.security.oauth2.resourceserver.jwt.public-key-location property, where the value needs to point to a file containing the public key in the PEM-encoded x509 format.

The spring.security.oauth2.resourceserver.jwt.audiences property can be used to specify the expected values of the aud claim in JWTs.
For example, to require JWTs to contain an aud claim with the value my-audience:

Properties

spring.security.oauth2.resourceserver.jwt.audiences[0]=my-audience

Yaml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          audiences:
            - "my-audience"

The same properties are applicable for both servlet and reactive applications.
Alternatively, you can define your own JwtDecoder bean for servlet applications or a ReactiveJwtDecoder for reactive applications.

In cases where opaque tokens are used instead of JWTs, you can configure the following properties to validate tokens through introspection:

Properties

spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://example.com/check-token
spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id
spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret

Yaml

spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: "https://example.com/check-token"
          client-id: "my-client-id"
          client-secret: "my-client-secret"

Again, the same properties are applicable for both servlet and reactive applications.
Alternatively, you can define your own OpaqueTokenIntrospector bean for servlet applications or a ReactiveOpaqueTokenIntrospector for reactive applications.

If you have spring-security-oauth2-authorization-server on your classpath, you can take advantage of some auto-configuration to set up a Servlet-based OAuth2 Authorization Server.

You can register multiple OAuth2 clients under the spring.security.oauth2.authorizationserver.client prefix, as shown in the following example:

Properties

spring.security.oauth2.authorizationserver.client.my-client-1.registration.client-id=abcd
spring.security.oauth2.authorizationserver.client.my-client-1.registration.client-secret={noop}secret1
spring.security.oauth2.authorizationserver.client.my-client-1.registration.client-authentication-methods[0]=client_secret_basic
spring.security.oauth2.authorizationserver.client.my-client-1.registration.authorization-grant-types[0]=authorization_code
spring.security.oauth2.authorizationserver.client.my-client-1.registration.authorization-grant-types[1]=refresh_token
spring.security.oauth2.authorizationserver.client.my-client-1.registration.redirect-uris[0]=https://my-client-1.com/login/oauth2/code/abcd
spring.security.oauth2.authorizationserver.client.my-client-1.registration.redirect-uris[1]=https://my-client-1.com/authorized
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[0]=openid
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[1]=profile
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[2]=email
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[3]=phone
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[4]=address
spring.security.oauth2.authorizationserver.client.my-client-1.require-authorization-consent=true
spring.security.oauth2.authorizationserver.client.my-client-2.registration.client-id=efgh
spring.security.oauth2.authorizationserver.client.my-client-2.registration.client-secret={noop}secret2
spring.security.oauth2.authorizationserver.client.my-client-2.registration.client-authentication-methods[0]=client_secret_jwt
spring.security.oauth2.authorizationserver.client.my-client-2.registration.authorization-grant-types[0]=client_credentials
spring.security.oauth2.authorizationserver.client.my-client-2.registration.scopes[0]=user.read
spring.security.oauth2.authorizationserver.client.my-client-2.registration.scopes[1]=user.write
spring.security.oauth2.authorizationserver.client.my-client-2.jwk-set-uri=https://my-client-2.com/jwks
spring.security.oauth2.authorizationserver.client.my-client-2.token-endpoint-authentication-signing-algorithm=RS256

Yaml

spring:
  security:
    oauth2:
      authorizationserver:
        client:
          my-client-1:
            registration:
              client-id: "abcd"
              client-secret: "{noop}secret1"
              client-authentication-methods:
                - "client_secret_basic"
              authorization-grant-types:
                - "authorization_code"
                - "refresh_token"
              redirect-uris:
                - "https://my-client-1.com/login/oauth2/code/abcd"
                - "https://my-client-1.com/authorized"
              scopes:
                - "openid"
                - "profile"
                - "email"
                - "phone"
                - "address"
            require-authorization-consent: true
          my-client-2:
            registration:
              client-id: "efgh"
              client-secret: "{noop}secret2"
              client-authentication-methods:
                - "client_secret_jwt"
              authorization-grant-types:
                - "client_credentials"
              scopes:
                - "user.read"
                - "user.write"
            jwk-set-uri: "https://my-client-2.com/jwks"
            token-endpoint-authentication-signing-algorithm: "RS256"
The client-secret property must be in a format that can be matched by the configured PasswordEncoder.
The default instance of PasswordEncoder is created via PasswordEncoderFactories.createDelegatingPasswordEncoder().

The auto-configuration Spring Boot provides for Spring Authorization Server is designed for getting started quickly.
Most applications will require customization and will want to define several beans to override auto-configuration.

The following components can be defined as beans to override auto-configuration specific to Spring Authorization Server:

  • RegisteredClientRepository

  • AuthorizationServerSettings

  • SecurityFilterChain

  • com.nimbusds.jose.jwk.source.JWKSource<com.nimbusds.jose.proc.SecurityContext>

  • JwtDecoder

Spring Boot auto-configures an InMemoryRegisteredClientRepository which is used by Spring Authorization Server for the management of registered clients.
The InMemoryRegisteredClientRepository has limited capabilities and we recommend using it only for development environments.
For production environments, consider using a JdbcRegisteredClientRepository or creating your own implementation of RegisteredClientRepository.

4.4. SAML 2.0

4.4.1. Relying Party

If you have spring-security-saml2-service-provider on your classpath, you can take advantage of some auto-configuration to set up a SAML 2.0 Relying Party.
This configuration makes use of the properties under Saml2RelyingPartyProperties.

A relying party registration represents a paired configuration between an Identity Provider, IDP, and a Service Provider, SP.
You can register multiple relying parties under the spring.security.saml2.relyingparty prefix, as shown in the following example:

Properties

spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party1.singlelogout.url=https://myapp/logout/saml2/slo
spring.security.saml2.relyingparty.registration.my-relying-party1.singlelogout.response-url=https://remoteidp2.slo.url
spring.security.saml2.relyingparty.registration.my-relying-party1.singlelogout.binding=POST
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.verification.credentials[0].certificate-location=path-to-verification-cert
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.entity-id=remote-idp-entity-id1
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.sso-url=https://remoteidp1.sso.url

spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.verification.credentials[0].certificate-location=path-to-other-verification-cert
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.entity-id=remote-idp-entity-id2
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.sso-url=https://remoteidp2.sso.url
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.singlelogout.url=https://remoteidp2.slo.url
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.singlelogout.response-url=https://myapp/logout/saml2/slo
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.singlelogout.binding=POST

Yaml

spring:
  security:
    saml2:
      relyingparty:
        registration:
          my-relying-party1:
            signing:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            decryption:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            singlelogout:
               url: "https://myapp/logout/saml2/slo"
               response-url: "https://remoteidp2.slo.url"
               binding: "POST"
            assertingparty:
              verification:
                credentials:
                - certificate-location: "path-to-verification-cert"
              entity-id: "remote-idp-entity-id1"
              sso-url: "https://remoteidp1.sso.url"

          my-relying-party2:
            signing:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            decryption:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            assertingparty:
              verification:
                credentials:
                - certificate-location: "path-to-other-verification-cert"
              entity-id: "remote-idp-entity-id2"
              sso-url: "https://remoteidp2.sso.url"
              singlelogout:
                url: "https://remoteidp2.slo.url"
                response-url: "https://myapp/logout/saml2/slo"
                binding: "POST"

For SAML2 logout, by default, Spring Security’s Saml2LogoutRequestFilter and Saml2LogoutResponseFilter only process URLs matching /logout/saml2/slo.
If you want to customize the url to which AP-initiated logout requests get sent to or the response-url to which an AP sends logout responses to, to use a different pattern, you need to provide configuration to process that custom pattern.
For example, for servlet applications, you can add your own SecurityFilterChain that resembles the following:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration(proxyBeanMethods = false)
public class MySamlRelyingPartyConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
        http.saml2Login(withDefaults());
        http.saml2Logout((saml2) -> saml2.logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
            .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2")));
        return http.build();
    }

}

5. Spring Session

Spring Boot provides Spring Session auto-configuration for a wide range of data stores.
When building a servlet web application, the following stores can be auto-configured:

  • Redis

  • JDBC

  • Hazelcast

  • MongoDB

The servlet auto-configuration replaces the need to use @Enable*HttpSession.

If a single Spring Session module is present on the classpath, Spring Boot uses that store implementation automatically.
If you have more than one implementation, Spring Boot uses the following order for choosing a specific implementation:

  1. Redis

  2. JDBC

  3. Hazelcast

  4. MongoDB

  5. If none of Redis, JDBC, Hazelcast and MongoDB are available, we do not configure a SessionRepository.

When building a reactive web application, the following stores can be auto-configured:

  • Redis

  • MongoDB

The reactive auto-configuration replaces the need to use @Enable*WebSession.

Similar to the servlet configuration, if you have more than one implementation, Spring Boot uses the following order for choosing a specific implementation:

  1. Redis

  2. MongoDB

  3. If neither Redis nor MongoDB are available, we do not configure a ReactiveSessionRepository.

Each store has specific additional settings.
For instance, it is possible to customize the name of the table for the JDBC store, as shown in the following example:

Properties

spring.session.jdbc.table-name=SESSIONS

Yaml

spring:
  session:
    jdbc:
      table-name: "SESSIONS"

For setting the timeout of the session you can use the spring.session.timeout property.
If that property is not set with a servlet web application, the auto-configuration falls back to the value of server.servlet.session.timeout.

You can take control over Spring Session’s configuration using @Enable*HttpSession (servlet) or @Enable*WebSession (reactive).
This will cause the auto-configuration to back off.
Spring Session can then be configured using the annotation’s attributes rather than the previously described configuration properties.

6. Spring for GraphQL

If you want to build GraphQL applications, you can take advantage of Spring Boot’s auto-configuration for Spring for GraphQL.
The Spring for GraphQL project is based on GraphQL Java.
You’ll need the spring-boot-starter-graphql starter at a minimum.
Because GraphQL is transport-agnostic, you’ll also need to have one or more additional starters in your application to expose your GraphQL API over the web:

Starter Transport Implementation

spring-boot-starter-web

HTTP

Spring MVC

spring-boot-starter-websocket

WebSocket

WebSocket for Servlet apps

spring-boot-starter-webflux

HTTP, WebSocket

Spring WebFlux

spring-boot-starter-rsocket

TCP, WebSocket

Spring WebFlux on Reactor Netty

6.1. GraphQL Schema

A Spring GraphQL application requires a defined schema at startup.
By default, you can write «.graphqls» or «.gqls» schema files under src/main/resources/graphql/** and Spring Boot will pick them up automatically.
You can customize the locations with spring.graphql.schema.locations and the file extensions with spring.graphql.schema.file-extensions.

If you want Spring Boot to detect schema files in all your application modules and dependencies for that location,
you can set spring.graphql.schema.locations to "classpath*:graphql/**/" (note the classpath*: prefix).

In the following sections, we’ll consider this sample GraphQL schema, defining two types and two queries:

type Query {
    greeting(name: String! = "Spring"): String!
    project(slug: ID!): Project
}

""" A Project in the Spring portfolio """
type Project {
    """ Unique string id used in URLs """
    slug: ID!
    """ Project name """
    name: String!
    """ URL of the git repository """
    repositoryUrl: String!
    """ Current support status """
    status: ProjectStatus!
}

enum ProjectStatus {
    """ Actively supported by the Spring team """
    ACTIVE
    """ Supported by the community """
    COMMUNITY
    """ Prototype, not officially supported yet  """
    INCUBATING
    """ Project being retired, in maintenance mode """
    ATTIC
    """ End-Of-Lifed """
    EOL
}
By default, field introspection will be allowed on the schema as it is required for tools such as GraphiQL.
If you wish to not expose information about the schema, you can disable introspection by setting spring.graphql.schema.introspection.enabled to false.

6.2. GraphQL RuntimeWiring

The GraphQL Java RuntimeWiring.Builder can be used to register custom scalar types, directives, type resolvers, DataFetcher, and more.
You can declare RuntimeWiringConfigurer beans in your Spring config to get access to the RuntimeWiring.Builder.
Spring Boot detects such beans and adds them to the GraphQlSource builder.

Typically, however, applications will not implement DataFetcher directly and will instead create annotated controllers.
Spring Boot will automatically detect @Controller classes with annotated handler methods and register those as DataFetchers.
Here’s a sample implementation for our greeting query with a @Controller class:

Java

import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

@Controller
public class GreetingController {

    @QueryMapping
    public String greeting(@Argument String name) {
        return "Hello, " + name + "!";
    }

}

Kotlin

;

import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.stereotype.Controller

@Controller
class GreetingController {

    @QueryMapping
    fun greeting(@Argument name: String): String {
        return "Hello, $name!"
    }

}

6.3. Querydsl and QueryByExample Repositories Support

Spring Data repositories annotated with @GraphQlRepository and extending one of:

  • QuerydslPredicateExecutor

  • ReactiveQuerydslPredicateExecutor

  • QueryByExampleExecutor

  • ReactiveQueryByExampleExecutor

are detected by Spring Boot and considered as candidates for DataFetcher for matching top-level queries.

6.4. Transports

6.4.1. HTTP and WebSocket

The GraphQL HTTP endpoint is at HTTP POST /graphql by default.
The path can be customized with spring.graphql.path.

The HTTP endpoint for both Spring MVC and Spring WebFlux is provided by a RouterFunction bean with an @Order of 0.
If you define your own RouterFunction beans, you may want to add appropriate @Order annotations to ensure that they are sorted correctly.

The GraphQL WebSocket endpoint is off by default. To enable it:

  • For a Servlet application, add the WebSocket starter spring-boot-starter-websocket

  • For a WebFlux application, no additional dependency is required

  • For both, the spring.graphql.websocket.path application property must be set

Spring GraphQL provides a Web Interception model.
This is quite useful for retrieving information from an HTTP request header and set it in the GraphQL context or fetching information from the same context and writing it to a response header.
With Spring Boot, you can declare a WebInterceptor bean to have it registered with the web transport.

Spring MVC and Spring WebFlux support CORS (Cross-Origin Resource Sharing) requests.
CORS is a critical part of the web config for GraphQL applications that are accessed from browsers using different domains.

Spring Boot supports many configuration properties under the spring.graphql.cors.* namespace; here’s a short configuration sample:

Properties

spring.graphql.cors.allowed-origins=https://example.org
spring.graphql.cors.allowed-methods=GET,POST
spring.graphql.cors.max-age=1800s

Yaml

spring:
  graphql:
    cors:
      allowed-origins: "https://example.org"
      allowed-methods: GET,POST
      max-age: 1800s

6.4.2. RSocket

RSocket is also supported as a transport, on top of WebSocket or TCP.
Once the RSocket server is configured, we can configure our GraphQL handler on a particular route using spring.graphql.rsocket.mapping.
For example, configuring that mapping as "graphql" means we can use that as a route when sending requests with the RSocketGraphQlClient.

Spring Boot auto-configures a RSocketGraphQlClient.Builder<?> bean that you can inject in your components:

Java

@Component
public class RSocketGraphQlClientExample {

    private final RSocketGraphQlClient graphQlClient;

    public RSocketGraphQlClientExample(RSocketGraphQlClient.Builder<?> builder) {
        this.graphQlClient = builder.tcp("example.spring.io", 8181).route("graphql").build();
    }

Kotlin

@Component
class RSocketGraphQlClientExample(private val builder: RSocketGraphQlClient.Builder<*>) {

And then send a request:

Java

Mono<Book> book = this.graphQlClient.document("{ bookById(id: \"book-1\"){ id name pageCount author } }")
    .retrieve("bookById")
    .toEntity(Book.class);

Kotlin

val book = graphQlClient.document(
    """
    {
        bookById(id: "book-1"){
            id
            name
            pageCount
            author
        }
    }               
    """
)
    .retrieve("bookById").toEntity(Book::class.java)

6.5. Exception Handling

Spring GraphQL enables applications to register one or more Spring DataFetcherExceptionResolver components that are invoked sequentially.
The Exception must be resolved to a list of graphql.GraphQLError objects, see Spring GraphQL exception handling documentation.
Spring Boot will automatically detect DataFetcherExceptionResolver beans and register them with the GraphQlSource.Builder.

6.6. GraphiQL and Schema printer

Spring GraphQL offers infrastructure for helping developers when consuming or developing a GraphQL API.

Spring GraphQL ships with a default GraphiQL page that is exposed at "/graphiql" by default.
This page is disabled by default and can be turned on with the spring.graphql.graphiql.enabled property.
Many applications exposing such a page will prefer a custom build.
A default implementation is very useful during development, this is why it is exposed automatically with spring-boot-devtools during development.

You can also choose to expose the GraphQL schema in text format at /graphql/schema when the spring.graphql.schema.printer.enabled property is enabled.

7. Spring HATEOAS

If you develop a RESTful API that makes use of hypermedia, Spring Boot provides auto-configuration for Spring HATEOAS that works well with most applications.
The auto-configuration replaces the need to use @EnableHypermediaSupport and registers a number of beans to ease building hypermedia-based applications, including a LinkDiscoverers (for client side support) and an ObjectMapper configured to correctly marshal responses into the desired representation.
The ObjectMapper is customized by setting the various spring.jackson.* properties or, if one exists, by a Jackson2ObjectMapperBuilder bean.

You can take control of Spring HATEOAS’s configuration by using @EnableHypermediaSupport.
Note that doing so disables the ObjectMapper customization described earlier.

spring-boot-starter-hateoas is specific to Spring MVC and should not be combined with Spring WebFlux.
In order to use Spring HATEOAS with Spring WebFlux, you can add a direct dependency on org.springframework.hateoas:spring-hateoas along with spring-boot-starter-webflux.

8. What to Read Next

You should now have a good understanding of how to develop web applications with Spring Boot.
The next few sections describe how Spring Boot integrates with various data technologies, messaging systems, and other IO capabilities.
You can pick any of these based on your application’s needs.

Время на прочтение
34 мин

Количество просмотров 111K

Вы можете использовать эту статью, чтобы понять, как использовать Spring MVC для создания веб-сайтов или RESTful сервисов. А также получить обзор часто задаваемых вопросов, охватывающих наиболее распространенные задачи Spring MVC.

Примечание: Статья ~ 7500 слов, вероятно, не стоит читать ее на мобильном устройстве. Добавьте ее в закладки и вернитесь позже.

Содержание

  • Введение
  • HttpServlets 101
  • DispatcherServlet
  • Контроллеры — создание HTML
  • REST контроллеры — создание XML / JSON
  • Часто задаваемые вопросы
  • Заключение
  • Благодарности

Введение

Что такое Spring MVC?

Spring MVC — это веб-фреймворк Spring. Он позволяет создавать веб-сайты или RESTful сервисы (например, JSON/XML) и хорошо интегрируется в экосистему Spring, например, он поддерживает контроллеры и REST контроллеры в ваших Spring Boot приложениях.

Это не очень помогло, не так ли?

К счастью, есть и более длинный ответ: остальная часть этого документа.

(Если вы не уверены, что знаете что такое Spring или Spring Boot, вы можете сначала прочитать, Что такое Spring Framework?)

HttpServlets 101

При написании веб-приложений на Java с использованием Spring или без него (MVC/Boot) вы в основном имеете в виду написание приложений, которые возвращают два разных формата данных:

  1. HTML → Ваше веб-приложение создает HTML-страницы, которые можно просматривать в браузере.
  2. JSON/XML → Ваше веб-приложение предоставляет сервисы RESTful, которые генерируют JSON или XML. Сайты с большим количеством Javascript или даже другие веб-сервисы могут затем использовать данные, которые предоставляют эти сервисы.
  3. Да, есть и другие форматы данных и варианты использования, но пока мы их игнорируем.

Как бы вы написали такие приложения без каких-либо фреймворков? Только на простой Java?

На самом низком уровне каждое веб-приложение Java состоит из одного или нескольких HttpServlets. Они генерируют ваш HTML, JSON или XML. Фактически, каждый отдельный фреймворк из 1 миллиона доступных веб-фреймворков на Java (Spring MVC, Wicket, Struts) построена на основе HttpServlets.

(Примечание для придир: это может быть сделано без HttpServlets, но мы пока проигнорируем это.)

Создание HTML-страниц с помощью HttpServlets

Давайте посмотрим на супер простой HttpServlet, который возвращает очень простую статическую HTML-страницу.

package com.marcobehler.springmvcarticle;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyServletV1 extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        if (req.getRequestURI().equals("/")) {
            resp.setContentType("text/html");
            resp.getWriter().print("<html><head></head><body><h1>Welcome!</h1><p>This is a very cool page!</p></body></html>");
        }
        else {
            throw new IllegalStateException("Help, I don't know what to do with this url");
        }
    }
}

Давайте разберемся с этим кодом.

public class MyServletV1 extends HttpServlet {

Ваш сервлет расширяет класс Java HttpServlet.

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

Чтобы обработать (любой) запрос GET, вам необходимо переопределить метод doGet () из суперкласса. Для запросов POST вы должны переопределить doPost (). Аналогично для всех других HTTP методов.

if (req.getRequestURI().equals("/")) {

Ваш сервлет должен убедиться, что входящий URL-адрес является запросом, который он знает как обрабатывать. Пока сервлет обрабатывает только «/», то есть он обрабатывает www.marcobehler.com, но НЕ www.marcobehler.com/hello.

resp.setContentType("text/html");

Вам нужно установить правильный тип контента в ServletResponse, чтобы браузер знал, какой контент вы отправляете. В данном случае это HTML.

resp.getWriter().print("<html><head></head><body><h1>Welcome!</h1><p>This is a very cool page!</p></body></html>");

Помните: веб-сайты — это просто строки HTML! Поэтому вам нужно сгенерировать HTML-строку любым удобным вам способом и отправить ее обратно с помощью ServletResponse. Один из способов сделать это с помощью response writer.

После написания вашего сервлета вы должны зарегистрировать его в контейнере сервлетов, таком как Tomcat или Jetty. Если вы используете встроенную версию любого контейнера сервлета, весь код, необходимый для запуска вашего сервлета, будет выглядеть следующим образом:

package com.marcobehler.springmvcarticle;

import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;

public class TomcatApplicationLauncher {

    public static void main(String[] args) throws LifecycleException {
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);
        tomcat.getConnector();

        Context ctx = tomcat.addContext("", null);
        Wrapper servlet = Tomcat.addServlet(ctx, "myServlet", new MyServletV2());
        servlet.setLoadOnStartup(1);
        servlet.addMapping("/*");

        tomcat.start();
    }
}

Давайте разберемся с этим кодом.

Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
tomcat.getConnector();

Вы настраиваете новый сервер Tomcat, который будет слушать порт 8080.

        Context ctx = tomcat.addContext("", null);
        Wrapper servlet = Tomcat.addServlet(ctx, "myServlet", new MyServletV2());

Так вы регистрируете свой сервлет в Tomcat. Это первая часть, где вы просто сообщаете Tomcat о своем сервлете.

        servlet.addMapping("/*");

Вторая часть сообщает Tomcat, за какие запросы отвечает сервлет, то есть за отображение. Отображение /* означает, что оно отвечает за любой входящий запрос (/users, /register, /checkout).

        tomcat.start();

Вот и все. Теперь вы запускаете метод main(), переходите на порт 8080 в своем любимом веб-браузере (http://localhost:8080 /), и вы увидите красивую страницу HTML.

Таким образом, по сути, пока вы продолжаете расширять методы doGet () и doPost (), все ваше веб-приложение может состоять только из одного сервлета. Давайте попробуем это.

Создание JSON с помощью HttpServlets

Представьте себе, что помимо вашей (довольно пустой) HTML-страницы индекса вы теперь также хотите предложить REST API для вашего готовящегося к разработке внешнего интерфейса. Так что ваш интерфейс React или AngularJS будет вызывать URL-адрес примерно так:

/api/users/{userId}

Эта конечная точка должна возвращать данные в формате JSON для пользователя с заданным userId. Как мы могли бы доработать наш MyServlet для этого, опять же, без каких-либо фреймворков?

package com.marcobehler.springmvcarticle;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyServletV2 extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        if (req.getRequestURI().equals("/")) {
            resp.setContentType("text/html");
            resp.getWriter().print("<html><head></head><body><h1>Welcome!</h1><p>This is a very cool page!</p></body></html>");
        } else if (req.getRequestURI().startsWith("/api/users/")) {

            Integer prettyFragileUserId = Integer.valueOf(req.getRequestURI().lastIndexOf("/") + 1);

            resp.setContentType("application/json");

            // User user = dao.findUser(prettyFragileUserId)
            // actually: jsonLibrary.toString(user)
            resp.getWriter().print("{\n" +
                    "  \"id\":" + prettyFragileUserId + ",\n" +
                    "  \"age\": 55,\n" +
                    "  \"name\" : \"John Doe\"\n" +
                    "}");
        } else {
            throw new IllegalStateException("Help, I don't know what to do with this url");
        }
    }
}

Давайте разберемся с этим кодом.

        } else if (req.getRequestURI().startsWith("/api/users/")) {

Мы добавляем еще один if в наш метод doGet для обработки вызовов /api/users/.

  Integer prettyFragileUserId = Integer.valueOf(req.getRequestURI().lastIndexOf("/") + 1);

Мы делаем очень слабый разбор URL. Последняя часть URL — это идентификатор пользователя userID, например, 5 для /api/users/5. Здесь мы просто предполагаем, что пользователь всегда передает действительный int, что нам в действительности нужно проверить!

resp.setContentType("application/json");

Запись JSON в браузер означает установку правильного типа контента.

// User user = dao.findUser(prettyFragileUserId)
// actually: jsonLibrary.toString(user)
resp.getWriter().print("{\n" +
        "  \"id\":" + prettyFragileUserId + ",\n" +
        "  \"age\": 55,\n" +
        "  \"name\" : \"John Doe\"\n" +
        "}");

Опять же, JSON — это просто текст, поэтому мы можем записать его непосредственно в HTTPServletResponse. Возможно, мы бы использовали библиотеку JSON для преобразования нашего пользовательского Java-объекта в эту строку, но для простоты я не буду показывать это здесь.

Проблема с нашим подходом «один сервлет для всего»

Хотя наш сервлет выше работает, на горизонте вас ожидает немало проблем:

  1. Ваш сервлет должен выполнить множество ручных HTTP-специфических операций, проверять URI запроса, перебирать строки и т.д. Другими словами: ему нужно знать ЧТО хотят пользователи.
  2. Затем он также должен найти данные для всего, что вы хотите отобразить. Другими словами: это должно знать, КАК. В нашем примере выше это будет поиск пользователя в базе данных, которую мы для простоты закомментировали.
  3. Затем необходимо также преобразовать эти данные в JSON или HTML и установить соответствующие типы ответов.

Довольно много разных обязанностей, не так ли? Разве не было бы лучше, если бы вам не приходилось заботиться обо всем этом стандартом коде? Больше нет парсинга URI запроса и параметров, нет больше преобразований JSON, больше нет ответов сервлета?

Именно здесь на помощь приходит Spring MVC.

DispatcherServlet

Мы расскажем о Spring MVC немного нетрадиционно и не будем подробно останавливаться на том, что означает Model-View-Controller. Вместо этого немного раздразним вас.

Что если я скажу вам, что Spring MVC — это всего лишь один сервлет, как наш выше супер-сервлет?

Встречайте DispatcherServlet.

(О да, в этом, конечно, немного обмана)

Что делает Spring MVC DispatcherServlet?

Как уже упоминалось выше, почти все веб-фреймворки Java основаны на сервлетах, поэтому Spring MVC также нужен сервлет, который обрабатывает каждый входящий HTTP-запрос (поэтому DispatcherServlet также называется фронт-контроллером).

Что же в точности означает обрабатывать HTTP-запрос, точно? Представьте себе «рабочий процесс регистрации пользователя», при котором пользователь заполняет форму и отправляет ее на сервер и в ответ получает небольшую HTML страницу об успешной регистрации.

В этом случае ваш DispatcherServlet должен выполнить следующие действия:

  1. Необходимо просмотреть URI входящего запроса HTTP и любые параметры запроса. Например: POST /register?name=john&age33.
  2. Он должен потенциально преобразовывать входящие данные (параметры/тело запроса) в симпатичные маленькие объекты Java и перенаправить их в класс контроллер или REST контроллер,, который вы написали.
  3. Ваш контроллер сохраняет нового пользователя в базе данных, возможно отправляет электронное письмо и т.д. Он, скорее всего, делегирует это другому сервисному классу, но давайте предположим, что пока это происходит внутри контроллера.
  4. Он должен взять любой вывод из вашего контроллера и преобразовать его обратно в HTML/JSON/XML.

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

До сих пор изложение было немного расплывчато в отношении некоторых частей этого процесса. Что такое ModelAndView на рисунке выше? Как именно DispatcherServlet преобразует данные?

Как выглядит реальный процесс Let’s-write-HTML? Об этом узнаем в следующем разделе.

Контроллеры — создание HTML

Всякий раз, когда вы хотите написать HTML на клиенте, таком как браузер с Spring MVC (включая Spring Boot), вы захотите написать класс контроллера. Давайте сделаем это сейчас.

Как написать контроллер в Spring

Для нашего рабочего процесса регистрации пользователей выше (POST/register?name=john&age33) мы бы написали следующий класс.

package com.marcobehler.springmvcarticle;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class RegistrationController {

    @PostMapping("/register")
    public String registerUser(@RequestParam(required = false) Integer age, @RequestParam String name, Model model) {
        User user = new User(name, age);

        // TODO save user to database
        // userDao.save(user);

        // TODO send out registration email
        // mailService.sendRegistrationEmail(user);

        model.addAttribute("user", user);
        return "registration-success";
    }
}

Давайте разберемся с этим кодом.

@Controller
public class RegistrationController {

Класс контроллера в Spring просто аннотируется аннотацией Controller, ему не нужно реализовывать определенный интерфейс или расширяться из другого класса.

@PostMapping("/register")

Эта строка сообщает нашему DispatcherServlet, что всякий раз, когда приходит запрос POST для пути /register, включая любые параметры запроса (например, ?Username=), он должен отправлять запрос именно этому методу контроллера.

    public String registerUser(@RequestParam(required = false) Integer age, @RequestParam String name, Model model) {

Примечание Наименование нашего метода на самом деле не имеет значения, его можно назвать как угодно.
Однако мы указываем, что каждый запрос должен включать два параметра запроса, которые могут быть либо частью URL (?age=10&name=Joe), либо находиться в теле запроса POST. Кроме того, требуется только параметр name (параметр age является необязательным)

И параметр age, если пользователь предоставил его, автоматически преобразуется в Integer (исключение выдается, если предоставленное значение не является допустимым Integer)

Наконец, что не менее важно, Spring MVC автоматически внедряет параметр model в наш метод контроллера. Эта модель представляет собой простую карту, на которой вам нужно поместить все данные, которые вы хотите отобразить на вашей окончательной HTML-странице, но об этом чуть позже.

User user = new User(name, age);

// TODO save user to database
// userDao.save(user);

// TODO send out registration email
// mailService.sendRegistrationEmail(user);

Вы делаете все, что вам нужно сделать с данными входящего запроса. Создать пользователя, сохранить его в базе данных, отправить по электронной почте. Это ваша бизнес-логика.

model.addAttribute("user", user);

Вы добавляете своего пользователя в модель с ключом «user». Это означает, что вы сможете ссылаться на него в своем HTML-шаблоне позже, например, «${user.name}». Подробнее об этом через секунду.

return "registration-success";

Ваш метод возвращает простую строку со значением registration-success. Это не просто строка, это ссылка на ваше представление, т.е. шаблон HTML, который вы хотите, чтобы Spring отображал.

Views (представления)

Давайте пока проигнорируем, как (или, скорее, где) Spring MVC попытается найти это представление, т.е. ваш шаблон, вместо этого давайте посмотрим, как должен выглядеть ваш шаблон registration-success.html.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<p th:text="'Hello ' + ${user.name} + '!'"></p>

</body>
</html>

Это простая HTML-страница, которая содержит одну строку шаблона. Он печатает имя пользователя, который только что зарегистрировался.

<p th:text="'Hello ' + ${user.name} + '!'"></p>

Вопрос в том, что означает синтаксис th:text=? Он специфический для Spring? Или это что-то еще?

И ответ таков: Spring MVC ничего не знает о шаблонах HTML. Для работы с HTML-шаблонами требуется сторонняя библиотека шаблонов, и не нужно заботиться о том, какую библиотеку вы выберете.

В приведенном выше примере вы видите шаблон Thymeleaf, который очень популярен при работе над проектами Spring MVC.

Spring MVC и библиотеки шаблонов

Существует несколько различных библиотек шаблонов, которые хорошо интегрируются с Spring MVC, из которых вы можете выбрать: Thymeleaf, Velocity, Freemarker, Mustache и даже JSP (хотя это не библиотека шаблонов).

Фактически, вы должны явно выбрать библиотеку шаблонов, потому что если у вас нет такой библиотеки шаблонов, добавленной в ваш проект и настроенной правильно, то ваш метод контроллера не будет отображать вашу HTML-страницу — потому что он не знает, как это сделать.

Это также означает, что вы должны изучить и понять синтаксис конкретной библиотеки шаблонов в зависимости от проекта, в котором вы работаете, потому что все они немного отличаются друг от друга. Весело, правда?

Что такое ViewResolver?

На секунду давайте подумаем, где Spring на самом деле попытается найти ваши HTML-шаблоны, которые возвращает ваш контроллер.

Класс, который пытается найти ваш шаблон, называется ViewResolver. Поэтому всякий раз, когда запрос поступает в ваш контроллер, Spring проверяет настроенные ViewResolvers и запрашивает их, чтобы найти шаблон с заданным именем. Если у вас нет настроенных ViewResolvers, это не сработает.

Представьте, что вы хотите интегрироваться с Thymeleaf. Следовательно, вам нужен ThymeleafViewResolver.

package com.marcobehler.springmvcarticle;

import org.springframework.context.annotation.Bean;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;

public class ThymeleafConfig {

    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();

        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("classpath:/templates");
        templateResolver.setSuffix(".html");
        // some other lines neglected...

        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        // some other lines neglected...

        viewResolver.setTemplateEngine(templateEngine);
        return viewResolver;
    }
}

Давайте разберемся с этим кодом.

    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();

В конце концов, ThymeleafViewResolver просто реализует интерфейс Spring ViewResolver. Учитывая имя шаблона (помните: registration-success), ViewResolvers может найти фактический шаблон.

        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();

Для правильной работы ThymeleafViewResolver требуется несколько других специфичных для Thymeleaf классов. Одним из этих классов является SpringResourceTemplateResolver. Это делает фактическую работу по поиску вашего шаблона.

Примечание SpringResourceTemplateResolver является классом Thymeleaf

templateResolver.setPrefix("classpath:/templates");
templateResolver.setSuffix(".html");

По существу, вы говорите (с помощью синтаксиса Spring Resources): «Все мои шаблоны находятся по пути classpath, в папке /templates». И по умолчанию все они заканчиваются на .html. Это означает:

Всякий раз, когда наш контроллер возвращает String, подобный registration-success, ThymeleafViewResolver будет искать шаблон: classpath:/templates/registration-success.html.

Заметка на полях: Spring Boot

Вы можете подумать: Марко, мне никогда не приходилось настраивать такой ViewResolver, работая над проектами Spring Boot. И это правильно. Потому что Spring Boot автоматически настраивает его для вас каждый раз, когда вы добавляете в свой проект зависимость, такую как spring-boot-starter-thymeleaf.

Он также настраивает ViewResolver так, чтобы он по умолчанию просматривал ваш каталог src/main/resources/template.

Итак, Spring Boot действительно предварительно настраивает Spring MVC для вас. Запомните.

Резюме: Model-View-Controller

Посмотрев полный пример Controller & ViewResolver, становится намного проще говорить о концепции Spring Model-View-Controller.

  • С помощью нескольких аннотаций (Controller, @PostMapping, @RequestParam) вы можете написать контроллер, который будет заботиться о получении данных запроса и обрабатывать их соответствующим образом.
  • Ваша модель содержит все данные (и только данные), которые вы хотите отобразить в своем представлении. Это ваша работа, чтобы заполнить эту карту модели.
  • Ваше представление — это просто шаблон HTML. Неважно, откуда вы взяли данные (модели). Или каков текущий HTTP-запрос. Или даже если у вас есть активный HTTP-сеанс или нет.

Это все о разделении ответственностей.

На первый взгляд, немного перегруженный аннотациями, наш класс Spring контроллера читается намного лучше, с гораздо меньшим количеством подключений HTTP, чем наш супер-сервлет с самого начала.

Подробнее о контроллерах

Мы уже видели небольшое удобство, которое предоставляет нам Spring MVC при обработке входов HTTP.

  • Вам не нужно возиться с requestURI, вместо этого вы можете использовать аннотацию.
  • Вам не нужно возиться с преобразованиями типов параметров запроса или, разбираться является ли параметр необязательным или обязательным, вы можете использовать аннотацию вместо этого.

Давайте рассмотрим наиболее распространенные аннотации, которые помогут вам обрабатывать входящие HTTP-запросы.

@GetMapping и @RequestMappping

Вы уже видели аннотацию @GetMapping выше. Она эквивалентна аннотации *@RequestMapping*. Давайте рассмотрим пример:

@GetMapping("/books")
public void book() {
        //
}

/* these two mappings are identical */

@RequestMapping(value = "/books", method = RequestMethod.GET)
public void book2() {

}

@GetMapping, @[Post|Put|Delete|Patch]Mapping эквивалентно @RequestMapping(method=XXX). Это просто более новый способ (Spring 4.3+) для определения мапинга (связывания) с URL, поэтому вы найдете, что аннотация @RequestMapping часто используется в более старых, унаследованных проектах Spring.

@RequestParam

Для параметров HTTP-запроса, будь то в вашем URL (?Key=value) или в отправленном теле запроса формы, можно прочитать с помощью аннотации @RequestParam.

Вы уже видели, что он выполняет базовое преобразование типов (например, из параметра HTTP String в int), а также проверяет обязательные или дополнительные параметры.

@PostMapping("/users")   /* First Param is optional */
public User createUser(@RequestParam(required = false) Integer age, @RequestParam String name) {
   // does not matter
}

Если вы забудете указать в запросе обязательный параметр, вы получите код ответа 400 Bad Request и, при использовании Spring Boot, объект ошибки по умолчанию, который выглядит следующим образом:

{"timestamp":"2020-04-26T08:34:34.441+0000","status":400,"error":"Bad Request","message":"Required Integer parameter 'age' is not present","path":"/users"}

Если вы хотите еще большего удобства, вы можете позволить Spring напрямую преобразовывать все @RequestParams в объект без каких-либо необходимых аннотаций. Просто укажите ваш объект как «параметр метода«.

Вам просто нужно убедиться, что у вашего класса есть соответствующие методы getter/setter.

@PostMapping("/users")   /* Spring преобразует это автоматически, если у вас есть getters and setters */
public User createUser(UserDto userDto) {
    //
}

@PathVariable

Помимо параметров запроса, другим популярным способом указания переменных является непосредственное задание их в URI запроса, как @PathVariable. Поэтому, чтобы получить профиль пользователя с userId=123, вы должны вызвать следующий URL: GET / users/123

  1. Вам просто нужно убедиться, что значение вашего параметра соответствует значению между {} в аннотации сопоставления вашего запроса.

Кроме того, PathVariables также может быть обязательным или необязательным.

@GetMapping("/users/{userId}")
   public User getUser(@PathVariable(required = false) String userId) {
       // ...
       return user;
   }

И PathVariables, конечно, может быть напрямую преобразованы в Java-объект (при условии, что у объекта есть соответствующие методы getter/setter).

@GetMapping("/users/{userId}")
public User getUser(UserDto userDto) {
    // ...
    return user;
}

Резюме: Контроллеры

Короче говоря, при написании HTML-страниц с помощью Spring MVC вам придется сделать всего несколько вещей:

  1. Напишите свои контроллеры, «присыпанные» несколькими аннотациями. Spring позаботится о том, чтобы представить вам запрос ввода (параметры запроса, переменные пути) удобным способом.
  2. Выполните любую логику, необходимую для заполнения вашей модели. Вы можете удобно ввести модель в любой метод контроллера.
  3. Сообщите вашему контроллеру, какой шаблон HTML вы хотите отобразить, и верните имя шаблона в виде строки.
  4. Всякий раз, когда поступает запрос, Spring обязательно вызовет ваш метод контроллера и примет полученную модель и представление, отобразит его в HTML-строку и вернет его обратно в браузер.
  5. При условии, конечно, вы настроили соответствующую библиотеку шаблонов, что Spring Boot автоматически сделает для вас, если вы добавите необходимые зависимости в ваш проект.

Вот и все.

REST контроллеры — создание XML/JSON

Когда вы разрабатываете RESTFul сервисы, все немного по-другому. Ваш клиент, будь то браузер или другой веб-сервис, будет (обычно) создавать запросы JSON или XML. Клиент отправляет, скажем, запрос JSON, вы обрабатываете его, а затем отправитель ожидает возврата JSON.

Таким образом, отправитель может отправить вам этот фрагмент JSON как часть тела HTTP-запроса.

POST http://localhost:8080/users

###
{"email": "angela@merkel.de"}

Но на стороне Java (в вашей программе Spring MVC) вы не хотите иметь дело с JSON строками. Ни при получении запросов, как указано выше, ни при отправке ответов обратно клиенту. Вместо этого вы хотели бы просто иметь объекты Java, в которые Spring автоматически конвертирует JSON.

public class UserDto {
    private String email;
    //...
}

Это также означает, что вам не нужна вся эта обработка модели и представления, которые вам приходилось делать при рендеринге HTML в ваших контроллерах. Для RESTful сервисов у вас нет библиотеки шаблонов, читающей шаблон HTML и заполняющей его данными модели, чтобы сгенерировать для вас ответ JSON.

Вместо этого вы хотите перейти непосредственно из HTTP запросJava объект и из Java объектHTTP ответ.

Как вы уже догадались, это именно то, что Spring MVC обеспечивает при написании REST контроллера.

Как написать REST контроллер

Первое, что вам нужно сделать для вывода XML/JSON, это написать аннотацию @RestController вместо Controller. (Хотя @RestController является Controller, см. FAQ для точной разницы).

Если бы мы написали REST-контроллер для банка, который возвращает список транзакций пользователя, он мог бы выглядеть примерно так:

package com.marcobehler.springmvcarticle;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.List;

@RestController
public class BankController {

    @GetMapping("/transactions/{userId}")
    public List<Transaction> transactions(String userId) {

        // find transaction by user
        // List<Transaction> = dao.findByUserId(userId);

        List<Transaction> transactions = Collections.emptyList();
        return transactions;
    }
}

Давайте разберемся с этим кодом.

@RestController
public class BankController {

Вы снабдили класс BankController аннотацией @RestController, которая сообщает Spring, что вы не хотите писать HTML-страницы через обычный процесс ModelAndView. Вместо этого вы хотите записать XML/JSON (или какой-либо другой формат) непосредственно в тело ответа HTTP.

public List<Transaction> transactions(String userId) {

Ваш контроллер больше не возвращает String (представление). Вместо этого он возвращает List, который Spring необходимо преобразовать в соответствующую структуру JSON или XML. По сути, вы хотите, чтобы ваши Java объекты Transaction стали такими (кто-то жаждал фаст-фуд очень рано утром):

[
  {
    "occurred": "28.04.2020 03:18",
    "description": "McDonalds - Binging",
    "id": 1,
    "amount": 10
  },
  {
    "occurred": "28.04.2020 06:18",
    "description": "Burger King - Never enough",
    "id": 2,
    "amount": 15
  }
]

Но как Spring MVC узнает, что ваш список транзакций должен быть преобразован в JSON? Почему не XML? Или YAML? Как ваш метод REST контроллер знает, каким должен быть предполагаемый формат ответа?

Для этого у Spring есть концепция согласования контента.

Короче говоря, согласование контента означает, что клиент должен сообщить вашему серверу, какой формат ответа он хочет получить от вашего REST контроллера.

Как? Указав заголовок Accept в HTTP-запросе.

GET http://localhost:8080/transactions/{userid}
Accept: application/json

Spring MVC разберет этот заголовок Accept и узнает: клиент хочет вернуть JSON (application/json), поэтому мне нужно преобразовать мой List в JSON. (Краткое примечание. Существуют и другие способы согласования содержимого, но заголовок Accept используется по умолчанию.)

Давайте назовем это согласование содержимого ответа, поскольку речь идет о формате данных ответа HTTP, который вы отправляете обратно своему клиенту.

Но согласование контента также работает для входящих запросов. Посмотрим как.

Согласование контента запроса — Content-Type Header (заголовок типа контента)

При создании RESTful API очень высока вероятность того, что ваши клиенты также смогут отправлять запросы в формате JSON или XML. Давайте снова возьмем пример из начала главы, где вы предлагаете конечную точку REST для регистрации новых пользователей:

POST http://localhost:8080/users

###
{"email": "angela@merkel.de"}

Как Spring узнает, что тело запроса выше содержит JSON, а не XML или YAML? Возможно, вы догадались, вам нужно добавить еще один заголовок, на этот раз это заголовок Content-Type.

POST ...

Content-Type: application/json; charset=UTF-8

###
...

Как будет выглядеть соответствующий REST контроллер для этого запроса?

package com.marcobehler.springmvcarticle;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BookingController {

    @PostMapping("/transactions")
    public Transaction transaction(@RequestBody TransactionDto dto) {
        // do something with the dto..create the booking..convert it to a transaction
        Transaction transaction = null;
        return transaction;
    }
}

Давайте разберемся с этим кодом.

    public Transaction transaction(@RequestBody TransactionDto dto) {

Подобно @RequestParam или @Pathvariable, вам понадобится другая аннотация, называемая @RequestBody.

@RequestBody в сочетании с правильным Content-Type будет сигнализировать Spring о том, что ему необходимо просмотреть тело HTTP-запроса и преобразовать его в любой Content-Type, указанный пользователем: JSON в нашем случае.

    // do something with the dto..create the booking..convert it to a transaction
    Transaction transaction = null;
    return transaction;
}

Тогда вашему методу больше не нужно заботиться об необработанной строке JSON, он может просто работать с TransactionDTO, сохранять его в базе данных, преобразовывать в объект Transaction, что угодно. В этом сила Spring MVC.

Сам Spring не может конвертировать форматы данных

Есть только одна небольшая проблема: Spring знает о заголовках Accept и Content-Type, но не знает, как конвертировать между объектами Java и JSON. Или XML. Или ЯМЛ.

Для этой грязной работы требуется соответствующая сторонняя библиотека (также называемая маршалинг / демаршаллинг или сериализация / десериализация.)

А классы, которые интегрируются между Spring MVC и этими сторонними библиотеками, называются HttpMessageConverters.

Что такое HttpMessageConverter?

HttpMessageConverter — это интерфейс с четырьмя методами (обратите внимание, я немного упростил интерфейс для более простого объяснения, так как он выглядит немного более продвинутым в реальной жизни).

  1. canRead (MediaType) → Может ли этот конвертер читать (JSON | XML | YAML | и т. д.)? Переданный здесь MediaType обычно является значением из заголовка запроса Content-Type.
  2. canWrite (MediaType) → Может ли этот преобразователь писать (JSON | XML | YAML | и т. д.)? Тип MediaType, переданный здесь, обычно является значением из заголовка запроса Accept.
  3. read(Object, InputStream, MediaType) → Читать мой Java-объект из (JSON | XML | YAML | и т. д.) InputStream
  4. write(Object, OutputStream, MediaType) → Записать мой Java-объект в OutputStream как (JSON | XML | YAML | и т. д.)

Короче говоря, MessageConverter должен знать, какие MediaTypes он поддерживает (например, application/json), а затем должен реализовать два метода для фактического чтения / записи в этом формате данных.

Какие есть HttpMessageConverters?

К счастью, вам не нужно писать эти конвертеры сообщений самостоятельно. Spring MVC поставляется с классом, который автоматически регистрирует пару стандартных HTTPMessageConverters для вас — если у вас есть соответствующие сторонние библиотеки в пути к классам.

Если вы не знаете об этом, это будет выглядеть как магия. В любом случае, взгляните на Spring AllEncompassingFormHttpMessageConverter (мне нравится это имя).

static {
        ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();
        jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
        jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
                                        ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
        jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
        jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
        gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
        jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
}

Давайте разберемся с этим кодом.

jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);

Spring MVC проверяет наличие класса javax.xml.bind.Binder и, если он есть, предполагает, что вы добавили в свой проект необходимую библиотеку для выполнения преобразований JAXB.

jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);

Spring MVC проверяет наличие двух классов ..jackson..ObjectMapper и ..jackson..JsonGenerator и, если это так, предполагает, что вы добавили библиотеку Jackson в свой проект для выполнения преобразований JSON.

jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);

Spring MVC проверяет наличие класса ..jackson..XmlMapper и, если это так, предполагает, что вы добавили поддержку XML библиотеки Jackson s в свой проект для выполнения преобразований XML.

И так далее. И через пару строк Spring просто добавляет HttpMessageConverter для каждой библиотеки, которую он «обнаружил».

if (jaxb2Present && !jackson2XmlPresent) {
        addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}

if (jackson2Present) {
        addPartConverter(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
        addPartConverter(new GsonHttpMessageConverter());
}

Заметка на полях: Spring Boot

При создании проектов Spring Boot вы автоматически будете использовать Spring MVC под капотом. Но Spring Boot также вызывает Jackson по умолчанию.

Вот почему вы можете сразу написать конечные точки JSON с помощью Spring Boot, потому что необходимые HttpMessageConverts будут добавлены автоматически для вас.

Резюме: REST контроллеры

По сравнению с HTML использование JSON / XML немного проще, так как вам не нужен рендеринг Model и View.

Вместо этого ваши контроллеры напрямую возвращают объекты Java, которые Spring MVC будет удобно сериализовать в JSON / XML или любой другой формат, который пользователь запросил с помощью HttpMessageConverters.

Однако вы должны убедиться в двух вещах, однако:

  1. Имеются соответствующие сторонние библиотеки на пути к классам.
  2. Отправлены правильные заголовки Accept или Content-Type с каждым запросом.

Часто задаваемые вопросы

Вы где-нибудь публиковали исходный код этой статьи?

Вы можете найти рабочий исходный код для большей части этой статьи в следующем репозитории GitHub:
https://github.com/marcobehler/spring-mvc-article

Просто клонируйте проект и запустите класс SpringMvcArticleApplication, чтобы запустить веб-приложение.

В чем разница между Spring MVC и Spring Boot?

Вкратце: нет никакой разницы, Spring Boot использует и строит приложение поверх Spring MVC.

Для более подробного объяснения вам нужно сначала прочитать статью Что такое Spring Framework?.

Какой самый быстрый способ создать новое приложение Spring MVC?

Если вы хотите упростить использование Spring MVC, самым быстрым способом будет создание нового Spring Boot проекта.

  1. Перейдите на сайт: https://start.spring.io/.
  2. Обязательно выберите Spring Web в качестве зависимости для вашего нового проекта.

Это позволит вам создавать веб / RESTful-приложения с помощью Spring MVC.

Какой тип ввода HTTP-запроса понимает Spring MVC?

Spring MVC понимает практически все, что предлагает HTTP — с помощью сторонних библиотек.

Это означает, что вы можете добавить в него тела запросов JSON, XML или HTTP (Multipart) Fileuploads, и Spring будет удобно конвертировать этот ввод в объекты Java.

Какие HTTP-ответы может создавать Spring MVC?

Spring MVC может записывать все что угодно в HttpServletResponse — с помощью сторонних библиотек.

Будь то HTML, JSON, XML или даже тела ответов WebSocket. Более того, он берет ваши объекты Java и генерирует эти тела ответов для вас.

В чем разница между контроллером и REST контроллером

  1. Контроллер по умолчанию возвращают HTML пользователям с помощью библиотеки шаблонов, если вы не добавите аннотацию @ResponseBody к определенным методам, которые также позволяют возвращать XML / JSON.
  2. Исходный код REST контроллера показывает, что на самом деле это контроллер с добавленной аннотацией @ResponseBody. Что эквивалентно написанию контроллера с аннотацией @ResponseBody для каждого метода.

@Controller
@ResponseBody
public @interface RestController {

  1. Поэтому REST контроллеры по умолчанию возвращает XML / JSON вместо HTML.

Примечание. XML и JSON — это просто самые популярные форматы данных, которые вы будете использовать в приложении Spring MVC. Однако ваши контроллеры / REST контроллеры могут возвращать что-либо еще, например, YAML. Вам нужно только убедиться, что правильный HttpMessageConverter зарегистрирован в вашем ApplicationContext.

Какую библиотеку шаблонов мне выбрать?

На протяжении многих лет я лично работал почти со всеми библиотеками шаблонов, и, хотя есть определенное стимулирование к использованию Thymeleaf в проектах Spring, у меня нет сильных предпочтений. Итак, либо воспользуйтесь Thymeleaf (если у вас нет опыта работы с другими системами), либо выберите тот, который вам наиболее удобен.

Почему мой контроллер выводит 404? Все мапинги верны.

Относительно распространенной ошибкой является то, что контроллер возвращает объекты, которые вы хотите преобразовать в JSON или XML, но вам не хватает аннотации @ResponseBody.

Spring возвратит довольно бессмысленное исключение 404 Not Found в этом случае.

package com.marcobehler.springmvcarticle;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class _404WithMissingResponseBodyController {

    @GetMapping("/users/{id}")   /* This won't work and lead to a 404 */
    public User getUser_404(@PathVariable String id) {
        return new User("Everyone's name is John", id);
    }

    @GetMapping("/users2/{id}")
    @ResponseBody  /* This will work */
    public User getUser_200(@PathVariable String id) {
        return new User("Everyone's name is John", id);
    }
}

Исправление: добавьте @ResponseBody или превратите ваш контроллер в REST контроллер.

Что произойдет, если вы определите один и тот же мапинг запросов для двух разных методов?

Если эти два метода имеют разные HTTP методы, это не будет проблемой.

/* это сработает */

@PostMapping("/users")
public void method1() {

}

@GetMapping("/users")
publi void method(2) {

}

Однако если вы сопоставите однотипные HTTP методы с одним и тем же путем, у вас возникнет проблема.

/* это не сработает */

@PostMapping("/users")
public void method1() {

}

@PostMapping("/users")
publi void method(2) {

}

При запуске приложения это приведет к исключению IllegalStateException, что намекает на ваше неоднозначное отображение.

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'howToPassAndRetrieveRequestParametersController' method
com.marcobehler.springmvcarticle.HowToPassAndRetrieveRequestParametersController#createUser(User)
to {POST /users3}: There is already 'howToPassAndRetrieveRequestParametersController' bean method

Нужно ли в URL кодировать @RequestParams?

Да, потому что Spring автоматически декодирует их из URL. Распространенная ошибка:

Представьте, что ваше приложение отправляет электронные письма с подтверждением всякий раз, когда новый пользователь регистрируется, и пользователь указывает знак «+» на своем адресе электронной почты, например, marco+wantsnospam@marcobehler.com.

@GetMapping("/confirm")
public void confirm(@RequestParam String email, @RequestParam String token){
    // confirm user...
}

Если вы забыли правильно закодировать знак ‘+’ в URL в своем письме-подтверждении и отправляете строку как есть на свой контроллер, какое значение будет содержать электронная почта @RequestParam?

Это будет «marco[space]wantnospam@marcobehler.com», так как Spring заменит + пробелом, что является правильной обработкой RFC3986.

Исправление: Убедитесь, что URL-адреса, которые вы вводите в свое приложение, правильно закодированы: marco%2Bwantsnospam@marcobehler.com, так как Spring будет автоматически их декодировать.

Как получить доступ к текущей HttpSession пользователя?

В Spring MVC контроллере или REST контроллере вы можете просто указать HttpSession в качестве аргумента метода, и Spring автоматически вставит его (создав его, если он еще не существует).

@RestController
public class HttpSessionController {

    @GetMapping("/session")
    public String getSession(HttpSession httpSession) {
        System.out.println("httpSession = " + httpSession);
        return httpSession.getId();
    }
}

Вы не можете сделать это со произвольными компонентами или сервисами, но вы все равно можете внедрить HttpSession в них.

@Service
class SomeOtherService {

    @Autowired
    private HttpSession httpSession;

    public HttpSession getHttpSession() {
        return httpSession;
    }
}

Как получить доступ к HttpServletRequest?

В вашем Spring MVC контроллере или REST контроллере вы можете просто указать HttpServletRequest в качестве аргумента метода, и Spring автоматически вставит его (создавая, если он еще не существует)

@RestController
public class HttpServletRequestController {

    @Autowired
    private SomeRequestService someRequestService;

    @GetMapping("/request")
    public String getRequest(HttpServletRequest request) {
        System.out.println("request = " + request);
        return request.toString();
    }
}

Вы не можете сделать это с произвольными компонентами или сервисами, но вы все еще можете внедрить HttpServletRequest в них.

@Service
class SomeRequestService {

    @Autowired
    private HttpServletRequest httpServletRequest;

    public HttpServletRequest getRequest() {
        return httpServletRequest;
    }
}

Как читать HTTP заголовки?

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

Какую бы версию вы ни выбрали, постарайтесь быть последовательным с вашим выбором.

package com.marcobehler.springmvcarticle;

import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;

import java.util.Map;

@Controller
public class HttpHeaderController {

    @GetMapping("/headers1")
    public void singleHeader(@RequestHeader("x-forwarded-for") String xForwardedFor) {
       // ...
    }

    @GetMapping("/headers2")
    public void headersAsMap(@RequestHeader Map<String,String> headers) {  // or MultiValueMap<String,String>
        // ...
    }

    @GetMapping("/headers3")
    public void headersAsObject(HttpHeaders headers) {
        // ...
    }
}

Как читать и писать cookie?

Для чтения файлов cookie вы можете использовать аннотацию @CookieValue в своих контроллерах. Вы должны будете писать cookie прямо в HttpServletResponse.

package com.marcobehler.springmvcarticle;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

@Controller
public class CookieController {

    @GetMapping("/cookie")
    public void handle(@CookieValue("JSESSIONID") String cookie, HttpServletResponse response) {

        response.addCookie(new Cookie("SOME_COOKIE_NAME", "This is a crazy new cookie!"));
        //...
    }
}

Как получить IP-адрес пользователя?

Это вопрос с подвохом. Существует метод с именем httpServletRequest.getRemoteAddr(), который, однако, возвращает только IP-адрес пользователя или последнего прокси-сервера, отправившего запрос, в 99,99% случаев это ваш Nginx или Apache.

Следовательно, вам нужно проанализировать заголовок X-Forwarded-For для получения правильного IP-адреса. Но что произойдет, если ваше приложение, кроме того, будет работать за CDN, например CloudFront? Тогда ваш X-Forwarded-For будет выглядеть так:

X-Forwarded-For: MaybeSomeSpoofedIp, realIp, cloudFrontIp

Проблема в том, что вы не можете прочитать заголовок слева направо, поскольку пользователи могут предоставить и, следовательно, подделать свой собственный заголовок X-Forwarded-For. Вам всегда нужно идти справа налево и исключать все известные IP-адреса. В случае CloudFront это означает, что вам необходимо знать диапазоны IP-адресов CloudFront и удалить их из заголовка. Ага!

Это приводит к довольно сложному коду, разрешающему IP. Угадайте, сколько проектов сделали это неправильно!

package com.marcobehler.springmvcarticle;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
public class IpController {

    private static final String[] HEADERS_TO_TRY = {
            "X-Forwarded-For",
            "Proxy-Client-IP",
            "WL-Proxy-Client-IP",
            "HTTP_X_FORWARDED_FOR",
            "HTTP_X_FORWARDED",
            "HTTP_X_CLUSTER_CLIENT_IP",
            "HTTP_CLIENT_IP",
            "HTTP_FORWARDED_FOR",
            "HTTP_FORWARDED",
            "HTTP_VIA",
            "REMOTE_ADDR"};

    @GetMapping("/ip")
    public String getClientIpAddress(HttpServletRequest request) {
        for (String header : HEADERS_TO_TRY) {
            String ip = request.getHeader(header);
            if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
                return getRealClientIpAddress(ip);
            }
        }
        return request.getRemoteAddr();
    }

    /**
     * Goes through the supplied ip string (could be one or multiple). Traverses it through the right side...
     * and removes any known ip address ranges
     *
     * @param ipString
     * @return
     */
    public String getRealClientIpAddress(String ipString) {
        String[] manyPossibleIps = ipString.split(",");

        for (int i = manyPossibleIps.length - 1; i >= 0; i--) {
            String rightMostIp = manyPossibleIps[i].trim();
            if (isKnownAddress(rightMostIp)) {
                continue; // skip this ip as it is trusted
            } else {
                return rightMostIp;
            }
        }

        return ipString;
    }

    private boolean isKnownAddress(String rightMostIp) {
        // do your check here..for cloudfront you'd need to download their ip address ranges
        // from e.g. http://d7uri8nf7uskq.cloudfront.net/tools/list-cloudfront-ips
        // and compare the current ip against them
        return false;
    }
}

Как вы можете управлять загрузкой файлов в приложении Spring MVC?

Предположим, что у вас есть правильная форма загрузки HTML-файла, которая выглядит примерно так:

<form method="POST" enctype="multipart/form-data" action="/upload">
    File to upload:<input type="file" name="file" />
    <input type="submit" value="Upload" />
</form>

Вам просто нужен контроллер с аннотацией @PostMapping и соответствующим параметром MultiPartFile, который содержит ваши данные для загрузки и удобные методы для сохранения файла на вашем диске.

package com.marcobehler.springmvcarticle;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

@Controller
public class FileUploadController {

    @PostMapping("/upload")
    public String handleFileUpload(@RequestParam MultipartFile file) throws IOException {
        // don't generate upload files like this in a real project.
        // give them random names and save their uploaded name as metadata in a database or similar
        final Path uploadDestination = Paths.get("C:\\uploads").resolve(file.getName());

        file.transferTo(uploadDestination);
        return "redirect:/";
    }
}

Как обрабатывать загрузку бинарных файлов (xls, pdf, csv, jpg, zip) с помощью Spring контроллеров?

Есть множество способов заставить это работать, от записи непосредственно в HttpServletResponse или возвращения массива byte[] в результате.

Тем не менее, самая Spring-и и гибкая версия заключается в возврате ‘ResponseEntity ‘. В зависимости от того, где вы сохранили файл, вы будете использовать различные ресурсы.

  • На диске → FileSystemResource
  • На пути к классам вашего проекта → ClassPathResource
  • Потоковая передача из «где-то» → InputStreamResource
  • Сделали его доступным как массив byte[] в памяти → ByteArrayResource

Все, что осталось сделать, это установить соответствующие HTTP-заголовки ответа (имя файла, тип контента и т.д.).

package com.marcobehler.springmvcarticle;

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;

@Controller
public class FileDownloadController {

    @RequestMapping(value = "/download/{jpgName}", method = RequestMethod.GET)
    public ResponseEntity<Resource> downloadJpg(
            @PathVariable String jpgName) throws IOException {

        //  Resource downloadResource = new InputStreamResource(soimeinputStream)
        //  Resource downloadResource = new ByteArrayResource(someByteArray)
        //  Resource downloadResource = new FileSystemResource(someFile)
        final ClassPathResource downloadResource = new ClassPathResource(jpgName);

        if (!downloadResource.exists()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
        }

        HttpHeaders headers = new HttpHeaders();

        // 1. set the correct content type
        headers.setContentType(MediaType.IMAGE_JPEG);

        // 2. set the correct content length, maybe stored in a db table
        headers.setContentLength(downloadResource.contentLength());

        // 3. if you want to force downloads, otherwise attachments might be displayed directly in the brwoser
        headers.setContentDispositionFormData("attachment", jpgName);

        return new ResponseEntity<>(downloadResource, headers, HttpStatus.OK);
    }
}

Как я могу глобально обрабатывать исключения в моих контроллерах?

В Spring MVC есть несколько способов обработки исключений, если вы не хотите обрабатывать их непосредственно в своих контроллерах, а в одном центральном месте.

Создайте класс ControllerAdvice или RestControllerAdvice в сочетании с аннотациями @ResponseStatus и @ExceptionHandler. Несолько замечаний:

  1. Вы можете догадаться о разнице между этими двумя классами, понимая разницу между контроллером и REST контроллером.
  2. @ResponseStatus позволяет вам определить код статуса HTTP, который должен быть возвращен клиенту после обработки вашего исключения.
  3. @ExceptionHandler указывает исключение, которое должно вызывать ваш метод-обработчик.
  4. Кроме этого, это все похоже на написание обычного контроллера или REST контроллера.

package com.marcobehler.springmvcarticle;

import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
public class GlobalControllerExceptionHandler {

    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(SomeConflictException.class)
    public String handleConflict(SomeConflictException e, Model model) {
        // do something
        model.addAttribute("message", e.getMessage());
        return "new-template";
    }

    @ResponseStatus(HttpStatus.NOT_IMPLEMENTED)  // 409
    @ExceptionHandler(NotYetImplementedExceptoin.class)
    public void handleBandwithLimitExceeded(NotYetImplementedExceptoin e) {
        // do nothing;
    }
}

Как вернуть любой код состояния (400, 404 и т.д.) из ваших контроллеров?

Создайте исключение ResponseStatusException с соответствующим кодом состояния и, возможно, причиной.

Альтернативой будет возвращение объекта ResponseEntity, но в большинстве случаев исключение лучше.

package com.marcobehler.springmvcarticle;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.server.ResponseStatusException;

@Controller
public class HttpStatusCodeController {

    @GetMapping("/somePath")
    public void alwaysThrowsException() {
         //throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Meeepp, not found.");

        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Well, that just wasn't right!");
    }
}

Как насчет концепции XYZ в Spring MVC?

Официальная документация Spring MVC буквально содержит сотни страниц, описывающих, как работает веб-фреймворк.

Поэтому, если вы хотите узнать больше о моделях, представлениях, ViewHandlers, InitBinders, RootContexts, фильтрах, кэшировании и т.д., я предлагаю вам прочитать документацию Spring MVC. Это просто выходит за рамки данного руководства, невозможно охватить это все.

Заключение

Это была хорошая прогулка. Наконец я надеюсь, что вы узнали множество вещей из этой статьи:

  • Spring MVC — это старый добрый MVC-фреймворк, который позволяет довольно легко писать веб-сайты HTML или JSON/XML веб-службы.
  • Он прекрасно интегрируется со множеством шаблонных библиотек и библиотек преобразования данных, а также с остальной частью экосистемы Spring, такой как Spring Boot.
  • Главным образом он позволяет вам сосредоточиться на написании своей бизнес-логики, не беспокоясь о стандартом коде сервлета, разборе HTTP-запросов / ответов и преобразовании данных.

Вот и все на сегодня. Спасибо за чтение.

Благодарности

Большое спасибо Patricio «Pato» Moschcovich, который не только выполнил корректуру этой статьи, но и дал бесценный отзыв!

Целью SpringMVC Framework является поддержка в Spring архитектуры модель-представление-контроллер (model-view-controller).  Spring обеспечивает готовые компоненты, которые могут быть использованы (и используются) для разработки веб-приложений.

Главной целью MVC является разделение объектов, бизнес-логики и внешнего вида приложения. Все эти компоненты слабо связаны между собой и при желании мы можем изменить, например, внешний вид приложения, не внося существенные изменения в остальные два компонента.

Итак, давайте пройдемся по каждому из этих блоков.

Модель (Model)

Этот блок инкапсулирует данные приложения. На практике это POJO-классы (Plain Old Java Objects – Простые старые Java-объекты).

Представление (View)

Модуль представления отвечает за вывод данных пользователю. Обычно это JSP файл, который может быть опознан и интерпретирован браузером на пользовательской машине.

Контроллер (Controller)

Контроллер отвечает за обработку запросов пользователей и передачу данных модулю View для обработки.

В основе Spring MVC Framework лежит DispatcherServlet, задача которого – обработка всех HTTP запросов и ответов. В понимании DispatcherServlet нам поможет следующий рисунок:

spring_dispatcherservlet

После получения HTTP-запроса DispatcherServlet (далее – DS) выполняет следующие действия.

  1. После получения HTTP-запроса DispatcherServlet даёт указание объекту Handling Mapping (обработка связывания), который вызывает следующий объект.
  2. DS посылает запрос контроллеру и вызывает соответствующие методы, в основе которых лежат методы GET и POST. Эти методы возвращают объект, в соответствии с бизнес-логикой метода и передают название (название ссылки) обратно в DS.
  3. C помощью View Resolver, DS подбирает необходимый вид для запроса.
  4. И, когда внешний вид сформирован, DS передаёт эти данные в модуль View, который обрабатывается браузером пользователя.

Все компоненты, указанные в рисунке, являются частями WebApplicationContext, который является расширением ApplicationContext + некоторые дополнительные функции.

Для того, чтобы понять, как всё это работает вместе, предлагаю рассмотреть пример простого приложения с использованием Spring MVC Framework.

Пример приложения:

Исходный код проекта можно скачать по ЭТОЙ ССЫЛКЕ.

Структура проекта

proselyteSpringMVCStructure

Блок Model

Developer.java


package net.proselyte.springmvc.model;

public class Developer {
    private int id;
    private String name;
    private String specialty;
    private int experience;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSpecialty() {
        return specialty;
    }

    public void setSpecialty(String specialty) {
        this.specialty = specialty;
    }

    public int getExperience() {
        return experience;
    }

    public void setExperience(int experience) {
        this.experience = experience;
    }

    @Override
    public String toString() {
        return "Developer{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", specialty='" + specialty + '\'' +
                ", experience=" + experience +
                '}';
    }
}

Блок Controller
DeveloperController


package net.proselyte.springmvc.controller;

import net.proselyte.springmvc.model.Developer;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class DeveloperController {
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index() {
        return "/index";
    }
    @RequestMapping(value = "developer", method = RequestMethod.GET)
    public ModelAndView developer() {
        return new ModelAndView("developer", "command", new Developer());
    }

    @RequestMapping(value = "/addDeveloper", method = RequestMethod.POST)
    public String addStudent(@ModelAttribute("mvc-dispatcher") Developer developer,
                             ModelMap model) {
        model.addAttribute("id", developer.getId());
        model.addAttribute("name", developer.getName());
        model.addAttribute("specilaty", developer.getSpecialty());
        model.addAttribute("experience", developer.getExperience());

        return "result";
    }
}

web.xml

<web-app version="2.4"
   xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
   http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

   <display-name>Spring MVC Application</display-name>

    <servlet>
      <servlet-name>mvc-dispatcher</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
   </servlet>

   <servlet-mapping>
      <servlet-name>mvc-dispatcher</servlet-name>
      <url-pattern>/</url-pattern>
   </servlet-mapping>
</web-app>

mvc-dispactcher-servlet.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="net.proselyte.springmvc.controller"/>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

Блок View

index.jsp

<%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Home Page</title>
</head>
<body>
  <h3><a href="/developer">Add Developer</a></h3>
</body>
</html>

developer.jsp


<%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<html>
<head>
  <title>Developer</title>
</head>
<body>

<h2>Enter developer information</h2>
<form:form method="post" action="addDeveloper">
  <table>
    <tr>
      <td><form:label path="id">Id</form:label></td>
      <td><form:input path="id" /></td>
    </tr>
    <tr>
      <td><form:label path="name">Name</form:label></td>
      <td><form:input path="name" /></td>
    </tr>
    <tr>
      <td><form:label path="specialty">Specialty</form:label></td>
      <td><form:input path="specialty" /></td>
    </tr>
    <tr>
      <td><form:label path="experience">experience</form:label></td>
      <td><form:input path="experience" /></td>
    </tr>
    <tr>
      <td colspan="2">
        <input type="submit" value="Submit"/>
      </td>
    </tr>
  </table>
</form:form>
</body>
</html>

result.jsp


<%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<html>
<head>
  <title>Developer Info</title>
</head>
<body>

<h2>Developer Information</h2>
<table>
  <tr>
    <td>Id</td>
    <td>${id}</td>
  </tr>
  <tr>
  <tr>
    <td>Name</td>
    <td>${name}</td>
  </tr>
  <tr>
    <td>Specialty</td>
    <td>${specilaty}</td>
  </tr>
  <tr>
    <td>Experience</td>
    <td>${experience}</td>
  </tr>
</table>
</body>
</html>


Результат работы программы

Главная страница
index.jsp

Добавление разработчика

addDeveloper

Вывод информации

developerInfo

В этой статье мы ознакомились с основами Spring MVC Framework и создали небольшое веб-приложение.

<html lang=«en« xmlns:th=«http://www.thymeleaf.org«>

<!— Required meta tags —>

<meta name=«viewport« content=«width=device-width, initial-scale=1, shrink-to-fit=no«>

<link rel=«stylesheet« href=«https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css«

integrity=«sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm« crossorigin=«anonymous«>

<title>Hello, world!</title>

<nav class=«navbar navbar-expand-lg navbar-dark bg-dark«>

<a class=«navbar-brand« href=«#«>Электронный деканат</a>

<button class=«navbar-toggler« type=«button« data-toggle=«collapse« data-target=«#navbarsExample05«

aria-controls=«navbarsExample05« aria-expanded=«false« aria-label=«Toggle navigation«>

<span class=«navbar-toggler-icon«></span>

<div class=«collapse navbar-collapse« id=«navbarsExample05«>

<ul class=«navbar-nav mr-auto«>

<a class=«nav-link« href=«#«>Главная</a>

<li class=«nav-item active«>

<a class=«nav-link« href=«#«>Студенты <span class=«sr-only«>(current)</span></a>

<a class=«nav-link« href=«#«>Группы</a>

<a class=«nav-link« href=«#«>Кафедры</a>

<form class=«form-inline mt-2 mt-md-0«>

<input class=«form-control mr-sm-2« type=«text« placeholder=«Введите текст« aria-label=«Search«>

<button class=«btn btn-outline-success my-2 my-sm-0« type=«submit«>Поиск</button>

<div class=«py-5 text-center«>

<h2>Управление студентами</h2>

<p class=«lead«>На данной странице вы можете добавить, отредактировать поля или удалить студентов кафедры</p>

<div class=«col col-lg-3«>

<a th:href=«@{~/add_student}« class=«btn btn-lg btn-block btn-outline-primary« role=«button«

aria-disabled=«true«>Добавить студента</a>

<div class=«table-responsive«>

<table class=«table table-striped«>

<th class=«text-justify«>#</th>

<th class=«text-justify«>Фамилия</th>

<th class=«text-justify«>Имя</th>

<th class=«text-justify«>Отчество</th>

<th class=«text-justify«>Почта</th>

<th class=«text-justify«>Телефон</th>

<th class=«text-justify«>Адрес</th>

<tr th:each=«student : ${students}«>

<td class=«align-middle«><span th:text=«${student.id}«/></td>

<td class=«align-middle«><span th:text=«${student.lastName}«/></td>

<td class=«align-middle«><span th:text=«${student.firstName}«/></td>

<td class=«align-middle«><span th:text=«${student.patronymic}«/></td>

<td class=«align-middle«><span th:text=«${student.email}«/></td>

<td class=«align-middle«><span th:text=«${student.phone}«/></td>

<td class=«align-middle«><span th:text=«${student.address}«/></td>

<!— Optional JavaScript —>

<!— jQuery first, then Popper.js, then Bootstrap JS —>

<script src=«https://code.jquery.com/jquery-3.2.1.slim.min.js«

integrity=«sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN«

crossorigin=«anonymous«></script>

<script src=«https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js«

integrity=«sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q«

crossorigin=«anonymous«></script>

<script src=«https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js«

integrity=«sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl«

crossorigin=«anonymous«></script>

( Примечание редактора : При ~ 7500 словах вы, вероятно, не захотите пытаться читать это на мобильном устройстве. Добавьте его в закладки и вернитесь позже.)

Вступление

Что такое Spring MVC?

Spring MVC – это веб-фреймворк Spring. Он позволяет создавать веб-сайты или сервисы RESTful (например, JSON/XML) и хорошо интегрирован в экосистему Spring, например, он питает @Controllers и @RestControllers ваших приложений Spring Boot.

Это на самом деле не помогает, не так ли?

К счастью, есть также длинный ответ : Остальная часть этого документа.

(Если вы не уверены в том, что такое Spring или Spring Boot, вы можете прочитать Что Такое Spring Framework? , во-первых.)

Основы веб-Java: Http-сервлеты

При написании веб-приложений на Java, с Spring или без него (MVC/Boot), вы в основном говорите о написании приложений, которые возвращают два разных формата данных:

  1. HTML → Ваше веб-приложение создает HTML-страницы, которые можно просматривать в браузере.

  2. JSON/XML → Ваше веб-приложение предоставляет службы RESTful, которые создают JSON или XML. Веб-сайты с поддержкой Javascript или даже другие веб-сервисы могут затем использовать данные, предоставляемые этими сервисами.

  3. (Да, существуют и другие форматы данных и варианты использования, но пока мы их проигнорируем.)

Как бы вы написали такие приложения без каких-либо рамок? Просто с помощью простой Java?

Ответ на этот вопрос необходим для действительно понимания Spring MVC, поэтому не забегайте вперед, потому что вы думаете, что это не имеет ничего общего с Spring MVC.

Ответ

На самом низком уровне каждое веб-приложение Java состоит из одного или нескольких Httpсервлет . Они генерируют ваш HTML, JSON или XML.

Фактически, (почти) каждый фреймворк из 1 миллиона доступных веб-фреймворков Java ( Spring MVC , Калитка , Распорки ) встроена поверх Http-сервлетов.

Как писать HTML-страницы с помощью Http-сервлетов

Давайте взглянем на супер простой HttpServlet, который возвращает очень простую статическую HTML-страницу.

    package com.marcobehler.springmvcarticle;

    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;

    public class MyServletV1 extends HttpServlet {

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            if (req.getRequestURI().equals("/")) {
                resp.setContentType("text/html");
                resp.getWriter().print("

Welcome!

This is a very cool page!

"); } else { throw new IllegalStateException("Help, I don't know what to do with this url"); } } }

Давайте разберемся с этим.

    public class MyServletV1 extends HttpServlet {

Ваш сервлет расширяет Класс HttpServlet Java.

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

Чтобы обработать (любой) запрос GET, вам необходимо переопределить метод doGet() из суперкласса. Для запросов на публикацию вы бы переопределили doPost() . Аналогично для всех других HTTP-методов.

    if (req.getRequestURI().equals("/")) {

Ваш сервлет должен убедиться, что входящий URL-адрес является запросом, который он знает, как обрабатывать. На данный момент сервлет только обрабатывает “/”, т.е. он обрабатывает На данный момент сервлет только обрабатывает “/”, т.е. он обрабатывает , но НЕ

    resp.setContentType("text/html");

Вам необходимо установить правильный Тип содержимого в ServletResponse, чтобы браузер знал, какой контент вы отправляете. В данном случае это HTML.

    resp.getWriter().print("

Welcome!

This is a very cool page!

");

Помните: веб-сайты – это всего лишь HTML-строки! Таким образом, вам нужно сгенерировать HTML-строку, в любом случае, вы хотите, и отправить ее обратно с ServletResponse. Один из способов сделать это – обратиться к автору ответа.

После написания сервлета вы зарегистрируете его в контейнере сервлета, например Tomcat или Jetty . Если вы используете встроенную версию любого контейнера сервлетов, весь код, необходимый для запуска вашего сервлета, будет выглядеть следующим образом:

    package com.marcobehler.springmvcarticle;

    import org.apache.catalina.Context;
    import org.apache.catalina.LifecycleException;
    import org.apache.catalina.Wrapper;
    import org.apache.catalina.startup.Tomcat;

    public class TomcatApplicationLauncher {

        public static void main(String[] args) throws LifecycleException {
            Tomcat tomcat = new Tomcat();
            tomcat.setPort(8080);
            tomcat.getConnector();

            Context ctx = tomcat.addContext("", null);
            Wrapper servlet = Tomcat.addServlet(ctx, "myServlet", new MyServletV2());
            servlet.setLoadOnStartup(1);
            servlet.addMapping("/*");

            tomcat.start();
        }
    }

Давайте разберемся с этим.

    Tomcat tomcat = new Tomcat();
    tomcat.setPort(8080);
    tomcat.getConnector();

Вы настраиваете новый сервер Tomcat, который будет запускаться на порту 8080.

    Context ctx = tomcat.addContext("", null);
    Wrapper servlet = Tomcat.addServlet(ctx, "myServlet", new MyServletV2());

Вот как вы регистрируете свой сервлет в Tomcat. Это первая часть, где вы просто рассказываете Tomcat о своем сервлете.

    servlet.addMapping("/*");

Вторая часть – сообщить Tomcat, за какие запросы отвечает сервлет, т.Е. за сопоставление. Сопоставление /* означает, что оно отвечает за любой входящий запрос ( /пользователи , /зарегистрироваться , /оформить заказ ).

Это оно. Теперь вы запускаете свой метод main() , перейдите на порт 8080 в своем любимом веб-браузере ( http://localhost:8080/ ), и вы увидите красивую HTML-страницу.

Таким образом, по сути, до тех пор, пока вы продолжаете расширять свои методы doGet() и doPost() , все ваше веб-приложение может состоять всего из одного сервлета. Давайте попробуем это сделать.

Как писать конечные точки JSON с помощью Http-сервлетов

Представьте, что помимо вашей (довольно пустой) страницы HTML-индекса, вы теперь также хотите предложить REST API для вашего скоро разрабатываемого интерфейса. Таким образом, ваш интерфейс React или AngularJS будет вызывать URL-адрес, подобный этому:

Эта конечная точка должна возвращать данные в формате JSON для пользователя с заданным идентификатором пользователя. Как мы могли бы улучшить наш MyServlet , чтобы сделать это, опять же, без разрешенных фреймворков?

    package com.marcobehler.springmvcarticle;

    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;

    public class MyServletV2 extends HttpServlet {

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
            if (req.getRequestURI().equals("/")) {
                resp.setContentType("text/html");
                resp.getWriter().print("

Welcome!

This is a very cool page!

"); } else if (req.getRequestURI().startsWith("/api/users/")) { Integer prettyFragileUserId = Integer.valueOf(req.getRequestURI().lastIndexOf("/") + 1); resp.setContentType("application/json"); // User user = dao.findUser(prettyFragileUserId) // actually: jsonLibrary.toString(user) resp.getWriter().print("{\n" + " \"id\":" + prettyFragileUserId + ",\n" + " \"age\": 55,\n" + " \"name\" : \"John Doe\"\n" + "}"); } else { throw new IllegalStateException("Help, I don't know what to do with this url"); } } }

Давайте разберемся с этим.

    } else if (req.getRequestURI().startsWith("/api/users/")) {

Мы добавляем еще один if в наш метод doGet для обработки вызовов /api/пользователей/|.

    Integer prettyFragileUserId = Integer.valueOf(req.getRequestURI().lastIndexOf("/") + 1);

Мы выполняем некоторый чрезвычайно хрупкий синтаксический анализ URL-адресов. Последняя часть URL-адреса – это идентификатор пользователя, например 5 для /api/пользователей/5 . Мы просто предполагаем здесь, что пользователь всегда передает действительный int, который вам действительно нужно будет проверить!

    resp.setContentType("application/json");

Запись JSON в браузер означает установку правильного типа содержимого.

    // User user = dao.findUser(prettyFragileUserId)
    // actually: jsonLibrary.toString(user)
    resp.getWriter().print("{\n" +
            "  \"id\":" + prettyFragileUserId + ",\n" +
            "  \"age\": 55,\n" +
            "  \"name\" : \"John Doe\"\n" +
            "}");

Опять же, ДЖЕЙСОН – это просто текст, поэтому мы можем записать его непосредственно в HttpServletResponse. вероятно, вы бы использовали библиотеку JSON для преобразования нашего пользовательского Java-объекта в эту строку, но для простоты я не буду показывать это здесь.

Проблема с нашим подходом “Один сервлет для управления всеми”

В то время как наш сервлет выше работает, на горизонте возникает довольно много проблем:

  1. Вашему сервлету необходимо выполнить множество ручных операций, специфичных для HTTP, проверить URI запросов, разобраться со строками и т. Д. Другими словами: он должен знать ЧТО пользователи хотят делать.

  2. Затем ему также необходимо найти данные для того, что вы хотите отобразить. Другими словами: он должен знать КАК . В нашем примере выше это означало бы поиск пользователя в базе данных, которую мы удобно прокомментировали.

  3. Затем ему также необходимо преобразовать эти данные в JSON или в HTML и установить соответствующие типы ответов.

Довольно много разных обязанностей, а? Разве не было бы лучше, если бы вам не нужно было заботиться обо всей этой сантехнике? Больше никаких URI запросов и синтаксического анализа параметров, больше никаких преобразований JSON, больше никаких ответов сервлетов?

Это именно где появляется Spring MVC .

Что, если я скажу вам, что Spring MVC на самом деле всего лишь один сервлет, как наш uberservlet выше? (И да, это, конечно, немного ложь)

Познакомьтесь с Диспетчерским сервлетом .

Познакомьтесь с Диспетчерским сервлетом . Диспетчерский сервлет Spring MVC обрабатывает каждый входящий HTTP-запрос (он также называется front controller ). Познакомьтесь с

Познакомьтесь с || Диспетчерским сервлетом || . Диспетчерский сервлет Spring MVC обрабатывает || каждый || входящий HTTP-запрос (он также называется || front controller || ). Теперь, что означает || обрабатывать ||, например, поток HTTP-запросов?

Познакомьтесь с || Диспетчерским сервлетом || . Диспетчерский сервлет Spring MVC обрабатывает || каждый || входящий HTTP-запрос (он также называется || front controller || ). Теперь, что означает || handle ||, например, представьте себе “рабочий процесс регистрации пользователя”, когда пользователь заполняет форму и отправляет ее на сервер, чтобы вернуть небольшую HTML-страницу успеха. Примерный поток HTTP-запросов ctly?

Познакомьтесь с || Диспетчерским сервлетом || . Диспетчерский сервлет Spring MVC обрабатывает || каждый || входящий HTTP-запрос (он также называется || front controller || ). Теперь, что означает || handle ||, например, представьте себе “рабочий процесс регистрации пользователя”, когда пользователь заполняет форму и отправляет ее на сервер, чтобы вернуть небольшую HTML-страницу успеха. Образец HTTP в этом случае вашему диспетчерскому сервлету необходимо выполнить следующие действия: поток запросов точно?

  1. Познакомьтесь с Диспетчерским сервлетом . Диспетчерский сервлет Spring MVC обрабатывает

  2. каждый входящий HTTP-запрос (он также называется front controller ). Теперь, что означает handle , например, представьте себе “рабочий процесс регистрации пользователя”, когда пользователь заполняет форму и отправляет ее на сервер, чтобы вернуть небольшую HTML-страницу успеха. Образец HTTP R В этом случае вашему диспетчерскому сервлету необходимо выполнить подгонку, необходимо просмотреть URI входящего запроса метода HTTP и любые параметры запроса. следующие вещи: равномерно протекают? Познакомьтесь с Диспетчерским сервлетом

  3. . Диспетчерский сервлет Spring MVC обрабатывает каждый входящий HTTP-запрос (он также называется

  4. front controller ). Теперь, что означает handle

Обзор диспетчерского сервлета

Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно.

Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Как именно DispatcherServlet преобразует данные?

Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Как именно DispatcherServlet преобразует данные, легче всего понять, взглянув на реальный пример. ? Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Как именно DispatcherServlet преобразует данные, легче всего понять, взглянув на реальный пример. ? Например: Как вы пишете HTML-сайты с помощью Spring MVC? Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Как именно DispatcherServlet преобразует данные, легче всего понять, взглянув на реальный пример. ? Ибо давайте выясним это в следующем разделе. пример: Как вы пишете HTML-сайты с помощью Spring MVC?

Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Как именно DispatcherServlet преобразует данные, легче всего понять, взглянув на реальный пример. ? Ибо давайте выясним это в следующем разделе. пример: Как вы пишете, как писать HTML с помощью @Controllers HTML-сайты с Spring MVC?

Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Как именно DispatcherServlet преобразует данные, легче всего понять, взглянув на реальный пример. ? Ибо давайте выясним это в следующем разделе. пример: Как вы пишете Всякий раз, когда вы хотите написать HTML для клиента, такого как браузер с Spring MVC (и это включает Spring Boot), вам захочется написать класс @Controller. Как писать HTML с помощью @Controllers HTML-сайты с Spring MVC? Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Как именно DispatcherServlet преобразует данные, легче всего понять, взглянув на реальный пример. ? Ибо давайте выясним это в следующем разделе. пример: Как вы пишете Всякий раз, когда хотите написать HTML для клиента, такого как браузер, давайте сделаем это сейчас. Spring MVC (и это включает в себя Spring Boot), вы захотите написать класс @Controller. Как писать HTML с помощью @Controllers HTML-сайты с Spring MVC?

Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Как именно DispatcherServlet преобразует данные, легче всего понять, взглянув на реальный пример. ? Ибо давайте выясним это в следующем разделе. пример: Как вы пишете Всякий раз, когда хотите написать HTML для клиента, такого как браузер, давайте сделаем это сейчас. Spring MVC (и это включает в себя Spring Boot, как написать @Controller в Spring t), вы захотите написать класс @Controller. Как писать HTML с помощью @Controllers HTML-сайты с Spring MVC?

Обзор DispatcherServlet Весь процесс выглядит следующим образом, при этом игнорируется достаточное количество промежуточных классов, поскольку DispatcherServlet не выполняет всю работу самостоятельно. Что такое представление модели на приведенном выше рисунке? Как именно DispatcherServlet преобразует данные, легче всего понять, взглянув на реальный пример. ? Ибо давайте выясним это в следующем разделе. пример: Как вы пишете Всякий раз, когда хотите написать HTML для клиента, такого как браузер, давайте сделаем это сейчас. Spring MVC (и это включает Spring Boot Для нашего рабочего процесса регистрации пользователей сверху ( ДОЛЖНОСТЬ и возраст 33 ), вы бы написали следующий класс. Как написать @Controller в Spring t), вам захочется написать класс @Controller. Как писать HTML с помощью @Controllers HTML-сайты с Spring MVC?

    package com.marcobehler.springmvcarticle;

    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;

    @Controller
    public class RegistrationController {

        @PostMapping("/register")
        public String registerUser(@RequestParam(required = false) Integer age, @RequestParam String name, Model model) {
            User user = new User(name, age);

            // TODO save user to database
            // userDao.save(user);

            // TODO send out registration email
            // mailService.sendRegistrationEmail(user);

            model.addAttribute("user", user);
            return "registration-success";
        }
    }

Давайте разберемся с этим.

    @Controller
    public class RegistrationController {

Класс контроллера в Spring просто аннотируется аннотацией @Controller , ему не нужно реализовывать определенный интерфейс или расширяться из другого класса.

    @PostMapping("/register")

Эта строка сообщает нашему диспетчерскому сервлету, что всякий раз, когда поступает запрос POST на путь /регистрация , включая любые параметры запроса (например, ?имя пользователя=), он должен отправить запрос этому самому методу контроллера.

    public String registerUser(@RequestParam(required = false) Integer age, @RequestParam String name, Model model) {

Примечание Наименование нашего метода ( регистрация пользователя ) на самом деле не имеет значения, его можно назвать как угодно.

Однако мы указываем, что каждый запрос должен включать два параметра запроса, которые могут быть частью URL-адреса ( ? возраст=10 и имя= Джо ) или быть в теле запроса на отправку. Кроме того, требуется только параметр имя (параметр возраст необязателен)

И параметр возраст , если пользователь его вводит, автоматически преобразуется в целое число (исключение выдается, если предоставленное значение не является допустимым целым числом)

И последнее, но не менее важное: Spring MVC автоматически вводит параметр model в наш метод контроллера. Эта модель представляет собой простую карту, на которой вам нужно разместить все данные, которые вы хотите отобразить на своей окончательной HTML-странице, но подробнее об этом через секунду.

    User user = new User(name, age);

    // TODO save user to database
    // userDao.save(user);

    // TODO send out registration email
    // mailService.sendRegistrationEmail(user);

Вы делаете все, что вам нужно, с входящими данными запроса. Создайте пользователя, сохраните его в базе данных, отправьте электронное письмо. Это ваша бизнес-логика.

    model.addAttribute("user", user);

Вы добавляете своего пользователя в модель в разделе “пользователь”. Это означает, что позже вы сможете ссылаться на него в своем HTML-шаблоне, например “${user.name }”. Подробнее об этом через секунду.

    return "registration-success";

Ваш метод возвращает простую строку с значение регистрация-успех . Это не просто какая-либо строка, это ссылка на ваше представление, то есть HTML-шаблон, который вы хотите, чтобы Spring отображал.

Как выглядят виды?

Давайте пока проигнорируем, как (или, скорее, где) Spring MVC попытается найти это представление, то есть ваш шаблон; вместо этого давайте посмотрим что ваш registration-success.html шаблон должен выглядеть так.

Это всего лишь простая HTML-страница, которая содержит одну строку шаблон-y . Он выводит имя пользователя, который только что зарегистрировался.

Вопрос в том, что это за синтаксис th:text= ? Это специфично для весны? Это что-то другое?

И ответ заключается в том, что Spring MVC на самом деле ничего не знает о HTML-шаблонах. Для выполнения всей работы по созданию шаблонов HTML требуется сторонняя библиотека шаблонов, и не обязательно важно, какую библиотеку вы выберете.

В приведенном выше случае вы смотрите на шаблон Thymeleaf , который является очень популярным выбором при работе над проектами Spring MVC.

Библиотеки Spring MVC и шаблонов

Существует несколько различных библиотек шаблонов, которые хорошо интегрируются с Spring MVC, из которых вы можете выбрать: Thymeleaf , Velocity , Freemarker , Усы и даже JSP (хотя это не библиотека шаблонов).

На самом деле, вы должны явно выбрать библиотеку шаблонов, потому что, если у вас нет такой библиотеки шаблонов, добавленной в ваш проект и настроенной правильно, то ваш метод @Controller не будет отображать вашу HTML-страницу – потому что он не будет знать, как это сделать.

Это также означает, что вы должны изучить и понять синтаксис конкретной библиотеки шаблонов в зависимости от проекта, в котором вы работаете, потому что все они немного отличаются друг от друга. Весело, правда?

Что такое решатель представлений?

На секунду давайте подумаем о том, где Spring на самом деле попытается найти ваши HTML-шаблоны, которые возвращает ваш @Controller.

Класс, который пытается найти ваш шаблон, называется ViewResolver . Поэтому всякий раз, когда запрос поступает в ваш контроллер, Spring просматривает настроенные решатели представлений и запрашивает их, чтобы найти шаблон с заданным именем. Если у вас не настроены какие-либо распознаватели представлений, это не сработает.

Представьте, что вы хотите интегрироваться с Thymeleaf . Следовательно, вам понадобится ThymeleafViewResolver.

    package com.marcobehler.springmvcarticle;

    import org.springframework.context.annotation.Bean;
    import org.thymeleaf.spring5.SpringTemplateEngine;
    import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
    import org.thymeleaf.spring5.view.ThymeleafViewResolver;

    public class ThymeleafConfig {

        @Bean
        public ThymeleafViewResolver viewResolver() {
            ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();

            SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
            templateResolver.setPrefix("classpath:/templates");
            templateResolver.setSuffix(".html");
            // some other lines neglected...

            SpringTemplateEngine templateEngine = new SpringTemplateEngine();
            templateEngine.setTemplateResolver(templateResolver);
            // some other lines neglected...

            viewResolver.setTemplateEngine(templateEngine);
            return viewResolver;
        }


    }

Давайте разберемся с этим.

    @Bean
    public ThymeleafViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();

В конце концов, ThymeleafViewResolver просто реализует интерфейс Spring ViewResolver . Учитывая имя шаблона (помните: регистрация прошла успешно ), распознаватели представлений могут найти фактический шаблон.

    SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();

Для правильной работы ThymeleafViewResolver требуется пара других классов, специфичных для Thymeleaf. Одним из таких классов является SpringResourceTemplateResolver . Он выполняет фактическую работу по поиску вашего шаблона.

Примечание

SpringResourceTemplateResolver – это класс Thymeleaf

templateResolver.setPrefix("classpath:/templates");
templateResolver.setSuffix(".html");

Вы в основном говорите (с помощью синтаксиса Spring Resources ): “Все мои шаблоны находятся в пути к классам, в папке /шаблоны “. И, по умолчанию, все они заканчиваются на .html . Это означает:

Всякий раз, когда ваш @Controller возвращает строку типа регистрация прошла успешно , ThymeleafViewResolver будет искать шаблон: classpath:/templates/registration-success.html .

Боковое примечание: Spring MVC, Пружинный ботинок и контроллеры

Возможно, вы думаете: Марко, мне никогда не приходилось настраивать такой ViewResolver за всю мою жизнь работая над проектами Spring Boot. И это правильно. Потому что Spring Boot автоматически настраивает его для вас , всякий раз, когда вы добавляете зависимость, такую как spring-boot-starter-thymeleaf в свой проект.

Он также настраивает ViewResolver для просмотра вашего каталога src/main/ресурсы/шаблон по умолчанию.

Итак, Spring Boot на самом деле просто предварительно настраивает Spring MVC для вас. Имейте это в виду.

Краткое описание: Модель-Представление-Контроллер

Увидев полный пример @Controller и ViewResolver, намного проще говорить о Spring M odel- V концепция iew- C контролера.

  • С помощью пары аннотаций (@Controller, @postmapping, @RequestParam) вы можете написать контроллер , который заботится о получении данных запроса и обрабатывает их соответствующим образом.

  • Ваша модель содержит все данные (и только данные), которые вы хотите отобразить в своем представлении. Ваша задача – заполнить эту модельную карту.

  • Ваш просмотр – это всего лишь HTML-шаблон. Ему все равно, откуда вы получили данные (модели). Или каков текущий HTTP-запрос. Или даже независимо от того, есть ли у вас активный HTTP-сеанс или нет.

Все дело в разделении забот.

Несмотря на то, что на первый взгляд наш класс Spring @Controller немного перегружен аннотациями, он читается намного лучше, с гораздо меньшим количеством подключений HTTP, чем наш uber-сервлет с самого начала.

Подробнее о контроллерах @

Мы уже видели некоторые удобства, которые Spring MVC предоставляет нам при обработке HTTP-входов.

  • Вам не нужно возиться с URI запроса, вместо этого вы можете использовать аннотацию.

  • Вам не нужно возиться с преобразованиями типов параметров запроса, или если параметр является необязательным или обязательным, вы можете вместо этого использовать аннотацию.

Давайте рассмотрим наиболее распространенные аннотации, которые помогут вам обрабатывать входящие HTTP-запросы.

@@Сопоставление с получением и @Сопоставление с запросом

Вы уже видели аннотацию @GetMapping выше. Он равен аннотации `@RequestMapping `. Давайте посмотрим, как:

    @GetMapping("/books")
    public void book() {
            //
    }

    /* these two mappings are identical */

    @RequestMapping(value = "/books", method = RequestMethod.GET)
    public void book2() {

    }

Сопоставление @GetMapping , Сопоставление @[Опубликовать|Поместить|Удалить|Исправить] эквивалентно сопоставлению @RequestMapping(метод=XXX) . Это просто более новый способ (Spring 4.3+) указания сопоставления, поэтому вы найдете аннотацию @RequestMapping, часто используемую в старых, устаревших проектах Spring.

@RequestParam запрос

Параметры HTTP-запроса, будь то в вашем URL ( ?ключ=значение ) или в теле запроса отправленной формы, можно прочитать с помощью аннотации @RequestParam .

Вы уже видели, что он выполняет базовое преобразование типов (например, из строкового параметра HTTP в int), а также проверяет наличие обязательных или необязательных параметров.

    @PostMapping("/users")   /* First Param is optional */
    public User createUser(@RequestParam(required = false) Integer age, @RequestParam String name) {
       // does not matter
    }

Если вы забудете указать необходимый параметр в своем запросе, вы получите 400 Неверный запрос код ответа и, при использовании Spring Boot, объект ошибки по умолчанию, который выглядит следующим образом:

    {"timestamp":"2020-04-26T08:34:34.441+0000","status":400,"error":"Bad Request","message":"Required Integer parameter 'age' is not present","path":"/users"}

Если вы хотите еще большего удобства, вы можете позволить Spring напрямую конвертировать все @RequestParams в объект без каких-либо необходимых аннотаций. Просто укажите свой объект как `параметр метода `.

Вам просто нужно убедиться, что в вашем классе есть соответствующие геттеры/сеттеры.

    @PostMapping("/users")   /* Spring will convert this automatically if you have getters and setters */
    public User createUser(UserDto userDto) {
        //
    }

@Переменный путь

Рядом с параметрами запроса другим популярным способом указания переменных будет непосредственно в URI запроса, как @PathVariable . Таким образом, для получения профиля пользователя с идентификатором пользователя=123 вы должны вызвать следующий URL: GET/users/123

    @GetMapping("/users/{userId}")  
    public User getUser(@PathVariable String userId) {
        // ...
        return user;
    }
  1. Вам просто нужно убедиться, что значение вашего параметра совпадает со значением {} в аннотации к запросу.

Кроме того, Переменные пути также могут быть обязательными или необязательными.

    @GetMapping("/users/{userId}")
       public User getUser(@PathVariable(required = false) String userId) {
           // ...
           return user;
       }

И переменные пути, конечно, могут быть напрямую переведены в объект Java (при условии, что объект имеет соответствующие геттеры/сеттеры).

    @GetMapping("/users/{userId}")
    public User getUser(UserDto userDto) {
        // ...
        return user;
    }

Резюме: @Контроллеры

Короче говоря, при написании HTML-страниц с помощью Spring MVC вам нужно будет сделать всего несколько вещей:

  1. Напишите свой @Controllers, посыпанный парой аннотаций. Spring позаботится о том, чтобы предоставить вам входные данные запроса (параметры запроса, переменные пути) удобным способом.

  2. Выполните любую логику, которая вам нужна для заполнения вашей модели. Вы можете удобно внедрить модель в любой метод контроллера.

  3. Сообщите вашему @Controller, какой HTML-шаблон вы хотите отобразить, и верните имя шаблона в виде строки.

  4. Всякий раз, когда поступает запрос, Spring обязательно вызывает ваш метод контроллера, берет полученную модель и представление, преобразует их в HTML-строку и возвращает обратно в браузер.

  5. (При условии, конечно, что вы настроили соответствующую библиотеку шаблонов, которую Spring Boot автоматически сделает за вас, если вы добавите необходимые зависимости в свой проект.)

Это оно.

Как написать XML/JSON с помощью @Restcontroller

Все немного по-другому, когда вы пишете службы RESTful. Ваш клиент, будь то браузер или другой веб-сервис, будет (обычно) создавать запросы JSON или XML. Клиент отправляет, скажем, запрос JSON, вы обрабатываете его, а затем отправитель ожидает возврата JSON.

Таким образом, отправитель может отправить вам этот фрагмент JSON как часть тела HTTP-запроса.

POST http://localhost:8080/users

###
{"email": "angela@merkel.de"}

Но на стороне Java (в вашей программе Spring MVC) вы не хотите связываться с необработанными строками JSON. Ни при получении запросов, подобных приведенным выше, ни при отправке ответов обратно клиенту.

Вместо этого вы хотели бы просто иметь объекты Java, которые автоматически преобразуются к весне.

    public class UserDto {
        private String email;
        //...
    }

Это также означает, что вам не нужна вся та обработка modelandview, которую вам приходилось выполнять при рендеринге HTML в ваших @контроллерах. Для служб RESTful у вас нет библиотеки шаблонов, которая считывает HTML-шаблон и заполняет его данными модели для генерации ответа JSON для вас.

Вместо этого вы хотите перейти непосредственно из HTTP-запроса → Java-объект и из Java-объекта → HTTP-ответ.

Как вы могли догадаться, это именно то, что Spring MVC позволяет вам делать, написав @Restcontroller .

Как написать @Restcontroller

Первое, что вам нужно сделать для вывода XML/JSON, это написать @RestController вместо @Controller. (Хотя @RestController ЯВЛЯЕТСЯ @контроллером, см. FAQ , в чем точная разница).

Если бы мы написали контроллер REST для банка, который возвращает список транзакций пользователя, это могло бы выглядеть примерно так:

    package com.marcobehler.springmvcarticle;

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.Collections;
    import java.util.List;

    @RestController
    public class BankController {

        @GetMapping("/transactions/{userId}")
        public List transactions(String userId) {

            // find transaction by user
            // List = dao.findByUserId(userId);

            List transactions = Collections.emptyList();
            return transactions;
        }
    }

Давайте разберемся с этим.

    @RestController
    public class BankController {

Вы аннотировали класс контроллера банка аннотацией @RestController , которая сигнализирует Spring о том, что вы не хотите писать HTML-страницы с помощью обычного процесса ModelAndView.

Вместо этого вы хотите записать XML/JSON (или какой-либо другой формат) непосредственно в тело ответа HTTP.

    public List transactions(String userId) {

Ваш контроллер больше не возвращает строку (представление). Вместо этого он возвращает Список<Транзакция> , который вы хотите, чтобы Spring преобразовал в соответствующую структуру JSON или XML. Вы в основном хотите, чтобы ваши объекты Java транзакции стали такими (кто-то очень рано утром проголодался по фаст-фуду).:

    [
      {
        "occurred": "28.04.2020 03:18",
        "description": "McDonalds - Binging",
        "id": 1,
        "amount": 10
      },
      {
        "occurred": "28.04.2020 06:18",
        "description": "Burger King - Never enough",
        "id": 2,
        "amount": 15
      }
    ]

Но как Spring MVC узнает, что ваш список транзакций должен быть преобразован в JSON? Почему бы не XML? Или YAML? Как ваш метод @RestController узнает, каким должен быть предполагаемый формат ответа?

Для этого в Spring есть концепция Согласования контента .

Как работает согласование содержимого (ответа): Принять заголовок

Короче говоря, согласование содержимого означает, что клиент должен сообщить вашему серверу, какой формат ответа он хочет получить от вашего @RestController.

Как? Указав заголовок Accept в HTTP-запросе.

GET http://localhost:8080/transactions/{userid}
Accept: application/json

Spring MVC посмотрит на это Примите заголовок и знайте: клиент хочет вернуть JSON (приложение/json), поэтому мне нужно преобразовать мой Список<Транзакция> в JSON. (Краткое примечание: существуют и другие способы выполнения согласования содержимого , но заголовок Accept используется по умолчанию.)

Давайте назовем это согласованием содержимого response , потому что речь идет о формате данных HTTP-ответа, который вы отправляете обратно своему клиенту.

Но согласование содержимого также работает для входящих запросов. Давайте посмотрим, как это сделать.

Как работает согласование содержимого запроса: Заголовок типа содержимого

При создании API-интерфейсов RESTful существует чрезвычайно высокая вероятность того, что вы также захотите, чтобы ваши клиенты могли отправлять данные в формате JSON или XML. Давайте снова рассмотрим пример из начала главы, где вы предлагаете конечную точку REST для регистрации новых пользователей:

POST http://localhost:8080/users

###
{"email": "angela@merkel.de"}

Как Spring узнает, что текст запроса выше содержит JSON, а не XML или YAML?

Возможно, вы правильно догадались, вам нужно будет добавить еще один заголовок, на этот раз это заголовок Content-Type .

POST ...

Content-Type: application/json; charset=UTF-8

###
...

Как будет выглядеть соответствующий метод @RestController для этого запроса?

    package com.marcobehler.springmvcarticle;

    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class BookingController {

        @PostMapping("/transactions")
        public Transaction transaction(@RequestBody TransactionDto dto) {
            // do something with the dto..create the booking..convert it to a transaction
            Transaction transaction = null;
            return transaction;
        }
    }

Давайте разберемся с этим.

    public Transaction transaction(@RequestBody TransactionDto dto) {

Подобно @RequestParam или @Pathvariable, вам понадобится другая аннотация, называемая @RequestBody .

@RequestBody в сочетании с правильным Типом содержимого будет сигнализировать Spring о том, что ему необходимо просмотреть тело HTTP-запроса и преобразовать его в любой тип содержимого, указанный пользователем: в нашем случае JSON.

        // do something with the dto..create the booking..convert it to a transaction
        Transaction transaction = null;
        return transaction;
    }

Тогда вашему методу больше не нужно заботиться о необработанной строке JSON, он может просто работать с транзакцией D, сохранять ее в базе данных, преобразовывать в объект транзакции, все, что вы хотите.

В этом сила Spring MVC.

Как Spring преобразует форматы данных?

Есть только одна небольшая проблема: Spring знает о заголовках типа Accept и Content, но не знает, как конвертировать между объектами Java и JSON. Или XML. Или YAML.

Для выполнения грязной работы требуется соответствующая сторонняя библиотека (также называемая сортировкой/отменой или сериализацией/десериализацией .)

И классы, которые интегрируются между Spring MVC и этими сторонними библиотеками, называются HttpMessageConverters .

Что такое HttpMessageConverter?

HttpMessageConverter – это интерфейс с четырьмя методами (обратите внимание, я немного упростил интерфейс для более простого объяснения, так как в реальной жизни он выглядит немного более продвинутым).

  1. может читать (тип носителя) = Может ли этот конвертер читать (JSON | XML | YAML | и т. Д.)? Тип носителя, передаваемый здесь, обычно является значением из заголовка запроса Тип содержимого .

  2. может записывать (Тип носителя) → Может ли этот конвертер записывать (JSON | XML | YAML | etc)? Тип носителя, передаваемый здесь, обычно является значением из заголовка запроса Accept .

  3. чтение (объект, входной поток, тип носителя) → Прочитайте мой Java-объект из (JSON | XML | YAML | и т. Д.) Входной поток

  4. запись (объект, поток вывода, тип носителя) → Запишите мой объект Java в поток вывода как (JSON | XML | YAML | и т. Д.)

Короче говоря, преобразователю сообщений необходимо знать, какие типы носителей он поддерживает (например, приложение/json), а затем необходимо реализовать два метода для фактического чтения/записи в этом формате данных.

Какие существуют HttpMessageConverters?

К счастью, вам не нужно писать эти конвертеры сообщений самостоятельно. Spring MVC поставляется с классом, который автоматически регистрирует для вас пару HttpMessageConverters по умолчанию – если у вас есть соответствующие сторонние библиотеки на пути к классу.

Если вы не знаете об этом, это будет звучать как волшебство. В любом случае, взгляните на Spring AllEncompassingFormHttpMessageConverter (Мне нравится это имя).

    static {
        ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();
        jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
        jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
                        ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
        jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
        jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
        gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
        jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
    }

Давайте разберемся с этим.

    jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);

Весенние проверки MVC если класс javax.xml.bind. Связующее присутствует, и если да, то предполагается, что вы добавили в свой проект необходимую библиотеку для выполнения преобразований JAXB .

    jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
                    ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);

Spring MVC проверяет, есть ли два класса ..Джексон.. Объектный преобразователь и ..Джексон.. JsonGenerator присутствуют, и если да, то предполагается, что вы добавили Джексон в ваш проект, чтобы выполнить преобразования JSON.

    jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);

Spring MVC проверяет, соответствует ли класс ..Джексону.. Xml-картограф присутствует, и если да, то предполагается, что вы добавили Поддержка XML Джексона в ваш проект для выполнения преобразований XML.

И так далее. И пару строк спустя Spring просто добавляет HttpMessageConverter для каждой библиотеки, которую он “обнаружил”.

    if (jaxb2Present && !jackson2XmlPresent) {
        addPartConverter(new Jaxb2RootElementHttpMessageConverter());
    }

    if (jackson2Present) {
        addPartConverter(new MappingJackson2HttpMessageConverter());
    }
    else if (gsonPresent) {
        addPartConverter(new GsonHttpMessageConverter());
    }

Побочное примечание: Контроллеры Spring MVC, Spring Boot и Rest

При создании проектов Spring Boot вы автоматически будете использовать Spring MVC под капотом. Но Spring Boot также по умолчанию подключает Джексона.

Это является причиной того, что вы можете немедленно написать конечные точки JSON с помощью Spring Boot, потому что правильные HttpMessageConverts будут добавлены автоматически для вас.

Краткое описание: @Restcontroller

По сравнению с потоком HTML поток JSON/XML немного проще, так как вы обходите весь этот рендеринг ModelAndView.

Вместо этого ваши @контроллеры напрямую возвращают объекты Java, которые Spring MVC удобно сериализует в JSON/XML или любой другой формат, запрошенный пользователем с помощью HttpMessageConverters.

Однако вам нужно убедиться в двух вещах:

  1. Имейте соответствующие сторонние библиотеки на пути к классам.

  2. Убедитесь, что вы отправили правильный Принимайте или заголовки типа содержимого с каждым запросом.

часто задаваемые вопросы

Вы где-нибудь публиковали исходный код этой статьи?

Вы можете найти рабочий исходный код для большей части этой статьи в следующем репозитории GitHub: https://github.com/marcobehler/spring-mvc-article

Просто клонируйте проект и запустите класс Spring Mvc Article Application для запуска веб-приложения.

В чем разница между Spring MVC и Spring Boot?

Короче говоря: нет разницы , Spring Boot использует и строит поверх Spring MVC.

Для более подробного объяснения вам нужно будет прочитать мой Что такое Spring Framework? статья первая.

Каков самый быстрый способ создать новое приложение Spring MVC?

Если вы не хотите использовать Spring MVC без него (в настоящее время это довольно редко), самым быстрым способом будет создание нового проекта Spring Boot.

  1. Перейти к Перейти к

  2. . Обязательно выберите Spring Web

Это позволит вам создавать веб-приложения/приложения RESTful с помощью Spring MVC.

Какой тип ввода HTTP-запроса понимает Spring MVC?

Spring MVC понимает в основном все, что предлагает HTTP – с помощью сторонних библиотек.

Это означает, что вы можете отправлять в него тела запросов JSON, XML или HTTP (составных) загрузок файлов, и Spring удобно преобразует эти входные данные в объекты Java.

Какие HTTP-ответы может написать Spring MVC?

Spring MVC может записать все, что вы хотите, в HttpServletResponse – с помощью сторонних библиотек.

Будь то HTML, JSON, XML или даже тела ответов WebSocket. Еще лучше то, что он берет ваши объекты Java и генерирует эти тела ответов для вас.

В чем разница между @Controller и @RestController

  1. @Controllers по умолчанию возвращает HTML вашим пользователям с помощью библиотеки шаблонов, если только вы не добавите аннотацию @ResponseBody к определенным методам, которые также позволяют возвращать XML/JSON.

  2. @@Restcontroller исходный код показывает, что он на самом деле является @контроллером с добавленной аннотацией @ResponseBody. Что эквивалентно написанию @контроллеров , в которых @ResponseBody аннотирован на каждом отдельном методе .

        @Controller
        @ResponseBody
        public @interface RestController {
  1. Поэтому @Restcontroller по умолчанию возвращает XML/JSON вместо HTML.

Примечание

XML и JSON – это просто самые популярные форматы данных, которые вы будете использовать в приложении Spring MVC. Однако ваши @Controllers/@Restcontroller могут возвращать что-либо еще, например YAML. Вам нужно только убедиться, что в вашем приложении зарегистрирован правильный HttpMessageConverter .

Какую библиотеку шаблонов мне следует выбрать?

На протяжении многих лет я лично работал почти со всеми библиотеками шаблонов, и, хотя существует определенный стимул использовать Thymeleaf в весенних проектах, у меня нет особых предпочтений. Итак, либо идите с Thymeleaf (если у вас нет опыта работы с какой-либо другой структурой) или выберите ту, которая вам наиболее удобна.

Почему мой @контроллер выводит 404? Все сопоставления верны.

Относительно распространенная ошибка заключается в том, что @Controller возвращает объекты, которые вы хотите преобразовать в JSON или XML, но вам не хватает аннотации @ResponseBody.

В этом случае Spring вернет довольно бессмысленное 404 Не найдено исключение.

    package com.marcobehler.springmvcarticle;

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.ResponseBody;

    @Controller
    public class _404WithMissingResponseBodyController {

        @GetMapping("/users/{id}")   /* This won't work and lead to a 404 */
        public User getUser_404(@PathVariable String id) {
            return new User("Everyone's name is John", id);
        }

        @GetMapping("/users2/{id}")
        @ResponseBody  /* This will work */
        public User getUser_200(@PathVariable String id) {
            return new User("Everyone's name is John", id);
        }
    }

Исправьте : Добавьте @ResponseBody или превратите свой @контроллер в @RestController .

Что произойдет, если вы определите одно и то же сопоставление запросов для двух разных методов?

Если два метода имеют разные методы HTTP, это не будет проблемой.

    /* this works */

    @PostMapping("/users")
    public void method1() {

    }

    @GetMapping("/users")
    publi void method(2) {

    }

Однако, если вы сопоставите одни и те же методы HTTP с одним и тем же путем, у вас возникнет проблема.

    /* this won't work */

    @PostMapping("/users")
    public void method1() {

    }

    @PostMapping("/users")
    public void method(2) {

    }

При запуске приложения это приведет к исключению IllegalStateException , намекающему на ваше неоднозначное сопоставление.

    Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'howToPassAndRetrieveRequestParametersController' method
    com.marcobehler.springmvcarticle.HowToPassAndRetrieveRequestParametersController#createUser(User)
    to {POST /users3}: There is already 'howToPassAndRetrieveRequestParametersController' bean method

Нужно ли мне кодировать URL-адрес @RequestParams?

Да, потому что Spring автоматически URL декодирует их. Распространенная ошибка:

Представьте, что ваше приложение отправляет электронные письма с подтверждением всякий раз, когда регистрируется новый пользователь, и пользователь регистрируется со знаком “+” в своем адресе электронной почты, например, marco+wantsnospam@marcobehler.com .

    @GetMapping("/confirm")
    public void confirm(@RequestParam String email, @RequestParam String token){
        // confirm user...
    }

Если вы забыли правильно закодировать URL-адрес со знаком + в своем письме с подтверждением и отправить строку как есть на ваш @Controller, какое значение будет содержать электронное письмо @RequestParam?

Это будет “Марко[космос] wantsnospam@marcobehler.com “, “, так как Spring заменит их + пробелом, что является правильной обработкой RFC 3986

Исправление : Убедитесь, что URL-адреса, которые вы отправляете в свое приложение, правильно закодированы: , так как Spring автоматически их декодирует.

Как получить доступ к текущей HttpSession пользователя?

В вашем Spring MVC @Controller или @RestController вы можете просто указать HttpSession в качестве аргумента метода, и Spring автоматически введет его (создав его, если он еще не существует)

    @RestController
    public class HttpSessionController {

        @GetMapping("/session")
        public String getSession(HttpSession httpSession) {
            System.out.println("httpSession = " + httpSession);
            return httpSession.getId();
        }
    }

Вы не можете сделать это со случайными @компонентами или @службами, но вы все равно можете @автоматически подключать к ним HttpSession.

    @Service
    class SomeOtherService {

        @Autowired
        private HttpSession httpSession;

        public HttpSession getHttpSession() {
            return httpSession;
        }
    }

Как получить доступ к запросу HttpServletRequest?

В вашем Spring MVC @Controller или @RestController вы можете просто указать HttpServletRequest в качестве аргумента метода, и Spring автоматически введет его (создав его, если он еще не существует)

    @RestController
    public class HttpServletRequestController {

        @Autowired
        private SomeRequestService someRequestService;

        @GetMapping("/request")
        public String getRequest(HttpServletRequest request) {
            System.out.println("request = " + request);
            return request.toString();
        }
    }

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

    @Service
    class SomeRequestService {

        @Autowired
        private HttpServletRequest httpServletRequest;

        public HttpServletRequest getRequest() {
            return httpServletRequest;
        }
    }

Как читать HTTP-заголовки?

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

Какую бы версию вы ни выбрали, старайтесь соответствовать своему выбору.

    package com.marcobehler.springmvcarticle;

    import org.springframework.http.HttpHeaders;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestHeader;

    import java.util.Map;

    @Controller
    public class HttpHeaderController {

        @GetMapping("/headers1")
        public void singleHeader(@RequestHeader("x-forwarded-for") String xForwardedFor) {
           // ...
        }

        @GetMapping("/headers2")
        public void headersAsMap(@RequestHeader Map headers) {  // or MultiValueMap
            // ...
        }

        @GetMapping("/headers3")
        public void headersAsObject(HttpHeaders headers) {
            // ...
        }


    }

Как читать и записывать файлы cookie?

Для чтения файлов cookie вы можете использовать аннотацию @CookieValue в своих контроллерах. Вам придется записывать файлы cookie непосредственно в HttpServletResponse.

    package com.marcobehler.springmvcarticle;

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.CookieValue;
    import org.springframework.web.bind.annotation.GetMapping;

    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletResponse;

    @Controller
    public class CookieController {

        @GetMapping("/cookie")
        public void handle(@CookieValue("JSESSIONID") String cookie, HttpServletResponse response) {

            response.addCookie(new Cookie("SOME_COOKIE_NAME", "This is a crazy new cookie!"));
            //...
        }
    }

Как получить IP-адрес пользователя?

Это вопрос с подвохом. Существует метод, называемый HttpServletRequest.getRemoteAddr() , который, однако, возвращает вам только IP пользователя или последнего прокси-сервера, отправившего запрос, которым в 99,99% случаев является ваш Nginx или Apache.

Следовательно, вам нужно будет проанализировать заголовок X-Forwarded-For для правильного IP-адреса. Но что произойдет, если ваше приложение, кроме того, будет работать за CDN, например CloudFront? Тогда ваш X-Forwarded-For будет выглядеть так:

X-Переадресованный Для: может быть, Какой-то Поддельный Ip, реальный Ip, облачный Ip

Проблема в том, что вы не можете прочитать заголовок слева направо, так как пользователи могут предоставить и, следовательно, подделать свой собственный заголовок X-Forwarded-For. Вы всегда должны идти от правильно влево и исключить все известные IP-адреса. В случае CloudFront это означает, что вам нужно знать диапазоны IP-адресов CloudFront и удалить их из заголовка. Ага!

Это приводит к довольно сложному коду разрешения IP-адресов. Угадайте, сколько проектов ошибаются в этом!

    package com.marcobehler.springmvcarticle;

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    import javax.servlet.http.HttpServletRequest;

    @RestController
    public class IpController {

        private static final String[] HEADERS_TO_TRY = {
                "X-Forwarded-For",
                "Proxy-Client-IP",
                "WL-Proxy-Client-IP",
                "HTTP_X_FORWARDED_FOR",
                "HTTP_X_FORWARDED",
                "HTTP_X_CLUSTER_CLIENT_IP",
                "HTTP_CLIENT_IP",
                "HTTP_FORWARDED_FOR",
                "HTTP_FORWARDED",
                "HTTP_VIA",
                "REMOTE_ADDR"};


        @GetMapping("/ip")
        public String getClientIpAddress(HttpServletRequest request) {
            for (String header : HEADERS_TO_TRY) {
                String ip = request.getHeader(header);
                if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
                    return getRealClientIpAddress(ip);
                }
            }
            return request.getRemoteAddr();
        }

        /**
         * Goes through the supplied ip string (could be one or multiple). Traverses it through the right side...
         * and removes any known ip address ranges
         *
         * @param ipString
         * @return
         */
        public String getRealClientIpAddress(String ipString) {
            String[] manyPossibleIps = ipString.split(",");

            for (int i = manyPossibleIps.length - 1; i >= 0; i--) {
                String rightMostIp = manyPossibleIps[i].trim();
                if (isKnownAddress(rightMostIp)) {
                    continue; // skip this ip as it is trusted
                } else {
                    return rightMostIp;
                }
            }

            return ipString;
        }

        private boolean isKnownAddress(String rightMostIp) {
            // do your check here..for cloudfront you'd need to download their ip address ranges
            // from e.g. http://d7uri8nf7uskq.cloudfront.net/tools/list-cloudfront-ips
            // and compare the current ip against them
            return false;
        }

    }

Как вы можете обрабатывать загрузку файлов в приложении Spring MVC?

Учитывая, что у вас есть правильная форма загрузки HTML-файла, которая выглядит примерно так:

Вам просто нужен @контроллер с соответствующим @Postmapping и параметром MultipartFile, который содержит ваши данные для загрузки и удобные методы сохранения файла на вашем диске.

    package com.marcobehler.springmvcarticle;

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.multipart.MultipartFile;


    import java.io.IOException;
    import java.nio.file.Path;
    import java.nio.file.Paths;

    @Controller
    public class FileUploadController {

        @PostMapping("/upload")
        public String handleFileUpload(@RequestParam MultipartFile file) throws IOException {
            // don't generate upload files like this in a real project.
            // give them random names and save their uploaded name as metadata in a database or similar
            final Path uploadDestination = Paths.get("C:\\uploads").resolve(file.getName());

            file.transferTo(uploadDestination);
            return "redirect:/";
        }

    }

Как обрабатывать байтовые загрузки (xls, pdf, csv, jpg, zip-файлы) с помощью контроллеров Spring?

Существует множество способов заставить это работать, начиная с прямой записи в HttpServletResponse или возврата байта[] в результате.

Однако наиболее упругой и гибкой версией является возврат *Идентификатор ответа<Ресурс>*\ s. В зависимости от того, где вы сохранили файл, вы могли бы использовать другой ресурс.

  • На диске → Файловая системаресурс

  • На пути к классу вашего проекта → ClassPathResource

  • Транслируйте его “откуда-нибудь” → InputStreamResource

  • Сделайте его доступным в виде байта[] в памяти → ByteArrayResource

Все, что осталось сделать, это установить соответствующие HTTP-заголовки ответа (имя файла, тип содержимого и т.д.).

    package com.marcobehler.springmvcarticle;

    import org.springframework.core.io.ClassPathResource;
    import org.springframework.core.io.Resource;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.server.ResponseStatusException;

    import java.io.IOException;

    @Controller
    public class FileDownloadController {

        @RequestMapping(value = "/download/{jpgName}", method = RequestMethod.GET)
        public ResponseEntity downloadJpg(
                @PathVariable String jpgName) throws IOException {

            //  Resource downloadResource = new InputStreamResource(soimeinputStream)
            //  Resource downloadResource = new ByteArrayResource(someByteArray)
            //  Resource downloadResource = new FileSystemResource(someFile)
            final ClassPathResource downloadResource = new ClassPathResource(jpgName);

            if (!downloadResource.exists()) {
                throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
            }

            HttpHeaders headers = new HttpHeaders();

            // 1. set the correct content type
            headers.setContentType(MediaType.IMAGE_JPEG);

            // 2. set the correct content length, maybe stored in a db table
            headers.setContentLength(downloadResource.contentLength());

            // 3. if you want to force downloads, otherwise attachments might be displayed directly in the brwoser
            headers.setContentDispositionFormData("attachment", jpgName);

            return new ResponseEntity<>(downloadResource, headers, HttpStatus.OK);
        }
    }

Как я могу обрабатывать исключения из моих @контроллеров по всему миру?

Существует буквально миллион способов обработки исключений с помощью Spring MVC, если вы не хотите обрабатывать их непосредственно в своих методах @Controller, а скорее в одном центральном месте.

Создайте класс @ControllerAdvice или @RestControllerAdvice в сочетании с аннотациями @ResponseStatus и @ExceptionHandler. Пара заметок:

  1. Вы можете догадаться о разнице между этими двумя, поняв разницу между @Controllers и @RestControllers.

  2. @ResponseStatus позволяет определить код состояния HTTP, который должен быть возвращен клиенту после обработки вашего исключения.

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

  4. В остальном это похоже на написание обычного @Controller или @RestController.

    package com.marcobehler.springmvcarticle;


    import org.springframework.http.HttpStatus;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseStatus;

    @ControllerAdvice
    public class GlobalControllerExceptionHandler {

        @ResponseStatus(HttpStatus.CONFLICT)  // 409
        @ExceptionHandler(SomeConflictException.class)
        public String handleConflict(SomeConflictException e, Model model) {
            // do something
            model.addAttribute("message", e.getMessage());
            return "new-template";
        }

        @ResponseStatus(HttpStatus.NOT_IMPLEMENTED)  // 409
        @ExceptionHandler(NotYetImplementedExceptoin.class)
        public void handleBandwithLimitExceeded(NotYetImplementedExceptoin e) {
            // do nothing;
        }
    }

Как вернуть любой код статуса (400, 404 и т.д.) с ваших контроллеров @?

Вызовите исключение ResponseStatusException с соответствующим кодом состояния и, возможно, причиной.

Альтернативой было бы возвращение объекта ResponseEntity, но в большинстве случаев исключение приятнее.

    package com.marcobehler.springmvcarticle;

    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.server.ResponseStatusException;

    @Controller
    public class HttpStatusCodeController {

        @GetMapping("/somePath")
        public void alwaysThrowsException() {
             //throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Meeepp, not found.");

            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Well, that just wasn't right!");
        }
    }

Как насчет концепции XYZ Spring MVC?

Официальная документация Spring MVC буквально содержит сотни страниц, описывающих, как работает веб-фреймворк.

Итак, если вы хотите узнать больше о моделях, Представлениях, Обработчиках представлений, Инициализации, корневых контекстах, Фильтрах, кэшировании и т. Д., Я приглашаю вас ознакомиться с этим. Это просто не входит в рамки данного руководства, чтобы охватить все.

Плавник

Это была настоящая поездка. В конце концов, я надеюсь, что вы убрали пару вещей из этой статьи:

  • Spring MVC – это старый добрый фреймворк MVC, который позволяет довольно легко создавать HTML-сайты или веб-сервисы JSON/XML.

  • Он хорошо интегрируется с множеством библиотек шаблонов и библиотек преобразования данных, а также с остальной частью экосистемы Spring, такой как Spring Boot.

  • В основном это позволяет вам сосредоточиться на написании бизнес-логики, не слишком беспокоясь о коде подключения сервлетов, анализе HTTP-запросов/ответов и преобразовании данных.

Вот и все на сегодня. Спасибо за чтение.

Подтверждения

Большое “спасибо” Патрисио “Пато” Мошковичу , который не только вычитал эту статью, но и предоставил бесценную обратную связь!

Оригинал: “https://dev.to/marcobehler/spring-mvc-in-depth-guide-4adf”

Понравилась статья? Поделить с друзьями:
  • Мануал по сборке акпп
  • Teac ud 505 инструкция на русском полное описание
  • Невотон элан инструкция по применению цена отзывы
  • Гириконд официальный сайт руководство
  • Акучек перформа инструкция к применению ошибка е9