diff --git a/src/docs/asciidoc/web/webflux-cors.adoc b/src/docs/asciidoc/web/webflux-cors.adoc index a19f06d276..33bd4b0e40 100644 --- a/src/docs/asciidoc/web/webflux-cors.adoc +++ b/src/docs/asciidoc/web/webflux-cors.adoc @@ -83,24 +83,43 @@ The {api-spring-framework}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`] annotation enables cross-origin requests on annotated controller methods, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -@RestController -@RequestMapping("/account") -public class AccountController { + @RestController + @RequestMapping("/account") + public class AccountController { - @CrossOrigin - @GetMapping("/{id}") - public Mono retrieve(@PathVariable Long id) { - // ... - } + @CrossOrigin + @GetMapping("/{id}") + public Mono retrieve(@PathVariable Long id) { + // ... + } - @DeleteMapping("/{id}") - public Mono remove(@PathVariable Long id) { - // ... + @DeleteMapping("/{id}") + public Mono remove(@PathVariable Long id) { + // ... + } + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @RestController + @RequestMapping("/account") + class AccountController { + + @CrossOrigin + @GetMapping("/{id}") + suspend fun retrieve(@PathVariable id: Long): Account { + // ... + } + + @DeleteMapping("/{id}") + suspend fun remove(@PathVariable id: Long) { + // ... + } } -} ---- By default, `@CrossOrigin` allows: @@ -119,48 +138,90 @@ should be used only where appropriate. `@CrossOrigin` is supported at the class level, too, and inherited by all methods. The following example specifies a certain domain and sets `maxAge` to an hour: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -@CrossOrigin(origins = "https://domain2.com", maxAge = 3600) -@RestController -@RequestMapping("/account") -public class AccountController { + @CrossOrigin(origins = "https://domain2.com", maxAge = 3600) + @RestController + @RequestMapping("/account") + public class AccountController { - @GetMapping("/{id}") - public Mono retrieve(@PathVariable Long id) { - // ... - } + @GetMapping("/{id}") + public Mono retrieve(@PathVariable Long id) { + // ... + } - @DeleteMapping("/{id}") - public Mono remove(@PathVariable Long id) { - // ... + @DeleteMapping("/{id}") + public Mono remove(@PathVariable Long id) { + // ... + } + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @CrossOrigin("https://domain2.com", maxAge = 3600) + @RestController + @RequestMapping("/account") + class AccountController { + + @GetMapping("/{id}") + suspend fun retrieve(@PathVariable id: Long): Account { + // ... + } + + @DeleteMapping("/{id}") + suspend fun remove(@PathVariable id: Long) { + // ... + } } -} ---- You can use `@CrossOrigin` at both the class and the method level, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -@CrossOrigin(maxAge = 3600) <1> -@RestController -@RequestMapping("/account") -public class AccountController { + @CrossOrigin(maxAge = 3600) // <1> + @RestController + @RequestMapping("/account") + public class AccountController { - @CrossOrigin("https://domain2.com") <2> - @GetMapping("/{id}") - public Mono retrieve(@PathVariable Long id) { - // ... - } + @CrossOrigin("https://domain2.com") // <2> + @GetMapping("/{id}") + public Mono retrieve(@PathVariable Long id) { + // ... + } - @DeleteMapping("/{id}") - public Mono remove(@PathVariable Long id) { - // ... + @DeleteMapping("/{id}") + public Mono remove(@PathVariable Long id) { + // ... + } + } +---- +<1> Using `@CrossOrigin` at the class level. +<2> Using `@CrossOrigin` at the method level. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @CrossOrigin(maxAge = 3600) // <1> + @RestController + @RequestMapping("/account") + class AccountController { + + @CrossOrigin("https://domain2.com") // <2> + @GetMapping("/{id}") + suspend fun retrieve(@PathVariable id: Long): Account { + // ... + } + + @DeleteMapping("/{id}") + suspend fun remove(@PathVariable id: Long) { + // ... + } } -} ---- <1> Using `@CrossOrigin` at the class level. <2> Using `@CrossOrigin` at the method level. @@ -191,26 +252,46 @@ should be used only where appropriate. To enable CORS in the WebFlux Java configuration, you can use the `CorsRegistry` callback, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -@Configuration -@EnableWebFlux -public class WebConfig implements WebFluxConfigurer { + @Configuration + @EnableWebFlux + public class WebConfig implements WebFluxConfigurer { - @Override - public void addCorsMappings(CorsRegistry registry) { + @Override + public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/api/**") - .allowedOrigins("https://domain2.com") - .allowedMethods("PUT", "DELETE") - .allowedHeaders("header1", "header2", "header3") - .exposedHeaders("header1", "header2") - .allowCredentials(true).maxAge(3600); + registry.addMapping("/api/**") + .allowedOrigins("https://domain2.com") + .allowedMethods("PUT", "DELETE") + .allowedHeaders("header1", "header2", "header3") + .exposedHeaders("header1", "header2") + .allowCredentials(true).maxAge(3600); - // Add more mappings... + // Add more mappings... + } + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun addCorsMappings(registry: CorsRegistry) { + + registry.addMapping("/api/**") + .allowedOrigins("https://domain2.com") + .allowedMethods("PUT", "DELETE") + .allowedHeaders("header1", "header2", "header3") + .exposedHeaders("header1", "header2") + .allowCredentials(true).maxAge(3600) + + // Add more mappings... + } } -} ---- @@ -232,25 +313,47 @@ for CORS. To configure the filter, you can declare a `CorsWebFilter` bean and pass a `CorsConfigurationSource` to its constructor, as the following example shows: -[source,java,indent=0] -[subs="verbatim"] +[source,java,indent=0,subs="verbatim",role="primary"] +.Java ---- -@Bean -CorsWebFilter corsFilter() { + @Bean + CorsWebFilter corsFilter() { - CorsConfiguration config = new CorsConfiguration(); + CorsConfiguration config = new CorsConfiguration(); - // Possibly... - // config.applyPermitDefaultValues() + // Possibly... + // config.applyPermitDefaultValues() - config.setAllowCredentials(true); - config.addAllowedOrigin("https://domain1.com"); - config.addAllowedHeader("*"); - config.addAllowedMethod("*"); + config.setAllowCredentials(true); + config.addAllowedOrigin("https://domain1.com"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); - return new CorsWebFilter(source); -} + return new CorsWebFilter(source); + } +---- +[source,kotlin,indent=0,subs="verbatim",role="secondary"] +.Kotlin +---- + @Bean + fun corsFilter(): CorsWebFilter { + + val config = CorsConfiguration() + + // Possibly... + // config.applyPermitDefaultValues() + + config.allowCredentials = true + config.addAllowedOrigin("https://domain1.com") + config.addAllowedHeader("*") + config.addAllowedMethod("*") + + val source = UrlBasedCorsConfigurationSource().apply { + registerCorsConfiguration("/**", config) + } + return CorsWebFilter(source) + } ---- diff --git a/src/docs/asciidoc/web/webflux-functional.adoc b/src/docs/asciidoc/web/webflux-functional.adoc index d943317f1f..17dc845034 100644 --- a/src/docs/asciidoc/web/webflux-functional.adoc +++ b/src/docs/asciidoc/web/webflux-functional.adoc @@ -28,41 +28,75 @@ difference that router functions provide not just data, but also behavior. `RouterFunctions.route()` provides a router builder that facilitates the creation of routers, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.web.reactive.function.server.RequestPredicates.*; -import static org.springframework.web.reactive.function.server.RouterFunctions.route; + import static org.springframework.http.MediaType.APPLICATION_JSON; + import static org.springframework.web.reactive.function.server.RequestPredicates.*; + import static org.springframework.web.reactive.function.server.RouterFunctions.route; -PersonRepository repository = ... -PersonHandler handler = new PersonHandler(repository); + PersonRepository repository = ... + PersonHandler handler = new PersonHandler(repository); -RouterFunction route = route() - .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) - .GET("/person", accept(APPLICATION_JSON), handler::listPeople) - .POST("/person", handler::createPerson) - .build(); + RouterFunction route = route() + .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) + .GET("/person", accept(APPLICATION_JSON), handler::listPeople) + .POST("/person", handler::createPerson) + .build(); -public class PersonHandler { + public class PersonHandler { - // ... - - public Mono listPeople(ServerRequest request) { // ... - } - public Mono createPerson(ServerRequest request) { - // ... - } + public Mono listPeople(ServerRequest request) { + // ... + } - public Mono getPerson(ServerRequest request) { - // ... + public Mono createPerson(ServerRequest request) { + // ... + } + + public Mono getPerson(ServerRequest request) { + // ... + } } -} ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val repository: PersonRepository = ... + val handler = PersonHandler(repository) + + val route = coRouter { // <1> + accept(APPLICATION_JSON).nest { + GET("/person/{id}", handler::getPerson) + GET("/person", handler::listPeople) + } + POST("/person", handler::createPerson) + } + + + class PersonHandler(private val repository: PersonRepository) { + + // ... + + suspend fun listPeople(request: ServerRequest): ServerResponse { + // ... + } + + suspend fun createPerson(request: ServerRequest): ServerResponse { + // ... + } + + suspend fun getPerson(request: ServerRequest): ServerResponse { + // ... + } + } +---- +<1> Create router using Coroutines router DSL, a Reactive alternative is also available via `router { }`. + One way to run a `RouterFunction` is to turn it into an `HttpHandler` and install it through one of the built-in <>: @@ -95,50 +129,88 @@ while access to the body is provided through the `body` methods. The following example extracts the request body to a `Mono`: -[source,java] +[source,java,role="primary"] +.Java ---- Mono string = request.bodyToMono(String.class); ---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val string = request.awaitBody() +---- -The following example extracts the body to a `Flux`, where `Person` objects are decoded from some -serialized form, such as JSON or XML: -[source,java] +The following example extracts the body to a `Flux` (or a `Flow` in Kotlin), +where `Person` objects are decoded from someserialized form, such as JSON or XML: + +[source,java,role="primary"] +.Java ---- Flux people = request.bodyToFlux(Person.class); ---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val people = request.bodyToFlow() +---- The preceding examples are shortcuts that use the more general `ServerRequest.body(BodyExtractor)`, which accepts the `BodyExtractor` functional strategy interface. The utility class `BodyExtractors` provides access to a number of instances. For example, the preceding examples can also be written as follows: -[source,java] +[source,java,role="primary"] +.Java ---- Mono string = request.body(BodyExtractors.toMono(String.class)); Flux people = request.body(BodyExtractors.toFlux(Person.class)); ---- +[source,kotlin,role="secondary"] +.Kotlin +---- + val string = request.body(BodyExtractors.toMono(String::class.java)).awaitFirst() + val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow() +---- The following example shows how to access form data: -[source,java] +[source,java,role="primary"] +.Java ---- -Mono map = request.body(BodyExtractors.toFormData()); +Mono map = request.formData(); +---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val map = request.awaitFormData() ---- The following example shows how to access multipart data as a map: -[source,java] +[source,java,role="primary"] +.Java ---- -Mono map = request.body(BodyExtractors.toMultipartData()); +Mono map = request.multipartData(); +---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val map = request.awaitMultipartData() ---- The following example shows how to access multiparts, one at a time, in streaming fashion: -[source,java] +[source,java,role="primary"] +.Java ---- Flux parts = request.body(BodyExtractors.toParts()); ---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val parts = request.body(BodyExtractors.toParts()).asFlow() +---- @@ -150,28 +222,48 @@ a `build` method to create it. You can use the builder to set the response statu headers, or to provide a body. The following example creates a 200 (OK) response with JSON content: -[source,java] +[source,java,role="primary"] +.Java ---- Mono person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class); ---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val person: Mono = ... +ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyWithType(person) +---- The following example shows how to build a 201 (CREATED) response with a `Location` header and no body: -[source,java] +[source,java,role="primary"] +.Java ---- URI location = ... ServerResponse.created(location).build(); ---- +[source,kotlin,role="secondary"] +.Kotlin +---- +val location: URI = ... +ServerResponse.created(location).build() +---- Depending on the codec used, it is possible to pass hint parameters to customize how the body is serialized or deserialized. For example, to specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON view]: ==== -[source,java] +[source,java,role="primary"] +.Java ---- ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...); ---- +[source,kotlin,role="secondary"] +.Kotlin +---- +ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).bodyWithType(...) +---- ==== @@ -180,11 +272,16 @@ ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.clas We can write a handler function as a lambda, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- HandlerFunction helloWorld = - request -> ServerResponse.ok().body(fromObject("Hello World")); + request -> ServerResponse.ok().bodyValue("Hello World"); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- +val helloWorld = HandlerFunction { ServerResponse.ok().bodyValue("Hello World") } ---- That is convenient, but in an application we need multiple functions, and multiple inline @@ -193,12 +290,11 @@ Therefore, it is useful to group related handler functions together into a handl has a similar role as `@Controller` in an annotation-based application. For example, the following class exposes a reactive `Person` repository: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.web.reactive.function.ServerResponse.ok; -import static org.springframework.web.reactive.function.BodyInserters.fromObject; +import static org.springframework.web.reactive.function.server.ServerResponse.ok; public class PersonHandler { @@ -221,7 +317,7 @@ public class PersonHandler { public Mono getPerson(ServerRequest request) { // <3> int personId = Integer.valueOf(request.pathVariable("id")); return repository.getPerson(personId) - .flatMap(person -> ok().contentType(APPLICATION_JSON).body(fromObject(person))) + .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person)) .switchIfEmpty(ServerResponse.notFound().build()); } } @@ -237,6 +333,37 @@ when the `Person` has been saved). variable. We retrieve that `Person` from the repository and create a JSON response, if it is found. If it is not found, we use `switchIfEmpty(Mono)` to return a 404 Not Found response. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + class PersonHandler(private val repository: PersonRepository) { + + suspend fun listPeople(request: ServerRequest): ServerResponse { // <1> + val people: Flow = repository.allPeople() + return ok().contentType(APPLICATION_JSON).bodyAndAwait(people); + } + + suspend fun createPerson(request: ServerRequest): ServerResponse { // <2> + val person = request.awaitBody() + repository.savePerson(person) + return ok().buildAndAwait() + } + + suspend fun getPerson(request: ServerRequest): ServerResponse { // <3> + val personId = request.pathVariable("id").toInt() + return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyAndAwait(it) } + ?: ServerResponse.notFound().buildAndAwait() + + } + } +---- +<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as +JSON. +<2> `createPerson` is a handler function that stores a new `Person` contained in the request body. +Note that `PersonRepository.savePerson(Person)` is a suspending function with no return type. +<3> `getPerson` is a handler function that returns a single person, identified by the `id` path +variable. We retrieve that `Person` from the repository and create a JSON response, if it is +found. If it is not found, we return a 404 Not Found response. [[webflux-fn-handler-validation]] @@ -246,34 +373,61 @@ A functional endpoint can use Spring's <> implementation for a `Person`: -==== -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -public class PersonHandler { + public class PersonHandler { - private final Validator validator = new PersonValidator(); // <1> + private final Validator validator = new PersonValidator(); // <1> - // ... + // ... - public Mono createPerson(ServerRequest request) { - Mono person = request.bodyToMono(Person.class).doOnNext(this::validate); <2> - return ok().build(repository.savePerson(person)); - } + public Mono createPerson(ServerRequest request) { + Mono person = request.bodyToMono(Person.class).doOnNext(this::validate); // <2> + return ok().build(repository.savePerson(person)); + } - private void validate(Person person) { - Errors errors = new BeanPropertyBindingResult(person, "person"); - validator.validate(person, errors); - if (errors.hasErrors) { - throw new ServerWebInputException(errors.toString()); <3> + private void validate(Person person) { + Errors errors = new BeanPropertyBindingResult(person, "person"); + validator.validate(person, errors); + if (errors.hasErrors()) { + throw new ServerWebInputException(errors.toString()); // <3> + } + } + } +---- +<1> Create `Validator` instance. +<2> Apply validation. +<3> Raise exception for a 400 response. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + class PersonHandler(private val repository: PersonRepository) { + + private val validator = PersonValidator() // <1> + + // ... + + suspend fun createPerson(request: ServerRequest): ServerResponse { + val person = request.awaitBody() + validate(person) // <2> + repository.savePerson(person) + return ok().buildAndAwait() + } + + private fun validate(person: Person) { + val errors: Errors = BeanPropertyBindingResult(person, "person"); + validator.validate(person, errors); + if (errors.hasErrors()) { + throw ServerWebInputException(errors.toString()) // <3> + } } } - ---- <1> Create `Validator` instance. <2> Apply validation. <3> Raise exception for a 400 response. -==== Handlers can also use the standard bean validation API (JSR-303) by creating and injecting a global `Validator` instance based on `LocalValidatorFactoryBean`. @@ -311,12 +465,21 @@ and so on. The following example uses a request predicate to create a constraint based on the `Accept` header: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -RouterFunction route = RouterFunctions.route() - .GET("/hello-world", accept(MediaType.TEXT_PLAIN), - request -> Response.ok().body(fromObject("Hello World"))); + RouterFunction route = RouterFunctions.route() + .GET("/hello-world", accept(MediaType.TEXT_PLAIN), + request -> ServerResponse.ok().bodyValue("Hello World")); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val route = coRouter { + (GET("/hello-world") and accept(MediaType.TEXT_PLAIN)).invoke { + ServerResponse.ok().bodyAndAwait("Hello World") + } + } ---- You can compose multiple request predicates together by using: @@ -353,8 +516,8 @@ There are also other ways to compose multiple router functions together: The following example shows the composition of four routes: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.web.reactive.function.server.RequestPredicates.*; @@ -379,6 +542,29 @@ RouterFunction route = route() `PersonHandler.createPerson`, and <4> `otherRoute` is a router function that is created elsewhere, and added to the route built. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import org.springframework.http.MediaType.APPLICATION_JSON + + val repository: PersonRepository = ... + val handler = PersonHandler(repository); + + val otherRoute: RouterFunction = coRouter { } + + val route = coRouter { + (GET("/person/{id}") and accept(APPLICATION_JSON)).invoke(handler::getPerson) // <1> + (GET("/person") and accept(APPLICATION_JSON)).invoke(handler::listPeople) // <2> + POST("/person").invoke(handler::createPerson) // <3> + }.and(otherRoute) // <4> +---- +<1> `GET /person/{id}` with an `Accept` header that matches JSON is routed to +`PersonHandler.getPerson` +<2> `GET /person` with an `Accept` header that matches JSON is routed to +`PersonHandler.listPeople` +<3> `POST /person` with no additional predicates is mapped to +`PersonHandler.createPerson`, and +<4> `otherRoute` is a router function that is created elsewhere, and added to the route built. === Nested Routes @@ -392,34 +578,58 @@ When using annotations, you would remove this duplication by using a type-level In WebFlux.fn, path predicates can be shared through the `path` method on the router function builder. For instance, the last few lines of the example above can be improved in the following way by using nested routes: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- RouterFunction route = route() - .path("/person", builder -> builder + .path("/person", builder -> builder // <1> .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) .GET("", accept(APPLICATION_JSON), handler::listPeople) .POST("/person", handler::createPerson)) .build(); ---- +<1> Note that second parameter of `path` is a consumer that takes the a router builder. -Note that second parameter of `path` is a consumer that takes the a router builder. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val route = coRouter { + "/person".nest { + (GET("/{id}") and accept(APPLICATION_JSON)).invoke(handler::getPerson) + (GET("") and accept(APPLICATION_JSON)).invoke(handler::listPeople) + POST("/person").invoke(handler::createPerson) + } + } +---- Though path-based nesting is the most common, you can nest on any kind of predicate by using the `nest` method on the builder. The above still contains some duplication in the form of the shared `Accept`-header predicate. We can further improve by using the `nest` method together with `accept`: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -RouterFunction route = route() - .path("/person", b1 -> b1 - .nest(accept(APPLICATION_JSON), b2 -> b2 - .GET("/{id}", handler::getPerson) - .GET("", handler::listPeople)) - .POST("/person", handler::createPerson)) - .build(); + RouterFunction route = route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET("", handler::listPeople)) + .POST("/person", handler::createPerson)) + .build(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val route = coRouter { + "/person".nest{ + accept(APPLICATION_JSON).nest { + GET("/{id}", handler::getPerson) + GET("", handler::listPeople) + POST("/person", handler::createPerson) + } + } + } ---- @@ -457,40 +667,72 @@ starter. The following example shows a WebFlux Java configuration (see <> for how to run it): -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -@Configuration -@EnableWebFlux -public class WebConfig implements WebFluxConfigurer { + @Configuration + @EnableWebFlux + public class WebConfig implements WebFluxConfigurer { + + @Bean + public RouterFunction routerFunctionA() { + // ... + } + + @Bean + public RouterFunction routerFunctionB() { + // ... + } - @Bean - public RouterFunction routerFunctionA() { // ... - } - @Bean - public RouterFunction routerFunctionB() { + @Override + public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + // configure message conversion... + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + // configure CORS... + } + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + // configure view resolution for HTML rendering... + } + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + @Bean + fun routerFunctionA(): RouterFunction<*> { + // ... + } + + @Bean + fun routerFunctionB(): RouterFunction<*> { + // ... + } + // ... - } - // ... + override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { + // configure message conversion... + } - @Override - public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { - // configure message conversion... - } + override fun addCorsMappings(registry: CorsRegistry) { + // configure CORS... + } - @Override - public void addCorsMappings(CorsRegistry registry) { - // configure CORS... + override fun configureViewResolvers(registry: ViewResolverRegistry) { + // configure view resolution for HTML rendering... + } } - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - // configure view resolution for HTML rendering... - } -} ---- @@ -506,8 +748,8 @@ The filter will apply to all routes that are built by the builder. This means that filters defined in nested routes do not apply to "top-level" routes. For instance, consider the following example: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- RouterFunction route = route() .path("/person", b1 -> b1 @@ -524,6 +766,12 @@ RouterFunction route = route() <1> The `before` filter that adds a custom request header is only applied to the two GET routes. <2> The `after` filter that logs the response is applied to all routes, including the nested ones. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // TODO when https://github.com/spring-projects/spring-framework/issues/23526 will be fixed +---- + The `filter` method on the router builder takes a `HandlerFilterFunction`: a function that takes a `ServerRequest` and `HandlerFunction` and returns a `ServerResponse`. @@ -535,27 +783,31 @@ Now we can add a simple security filter to our route, assuming that we have a `S can determine whether a particular path is allowed. The following example shows how to do so: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- + SecurityManager securityManager = ... -SecurityManager securityManager = ... - -RouterFunction route = route() - .path("/person", b1 -> b1 - .nest(accept(APPLICATION_JSON), b2 -> b2 - .GET("/{id}", handler::getPerson) - .GET("", handler::listPeople)) - .POST("/person", handler::createPerson)) - .filter((request, next) -> { - if (securityManager.allowAccessTo(request.path())) { - return next.handle(request); - } - else { - return ServerResponse.status(UNAUTHORIZED).build(); - } - }) - .build(); + RouterFunction route = route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET("", handler::listPeople)) + .POST("/person", handler::createPerson)) + .filter((request, next) -> { + if (securityManager.allowAccessTo(request.path())) { + return next.handle(request); + } + else { + return ServerResponse.status(UNAUTHORIZED).build(); + } + }) + .build(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // TODO when https://github.com/spring-projects/spring-framework/issues/23526 will be fixed ---- The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional. diff --git a/src/docs/asciidoc/web/webflux-view.adoc b/src/docs/asciidoc/web/webflux-view.adoc index 6ac30b7b01..09819fcd85 100644 --- a/src/docs/asciidoc/web/webflux-view.adoc +++ b/src/docs/asciidoc/web/webflux-view.adoc @@ -47,8 +47,8 @@ integration for using Spring WebFlux with FreeMarker templates. The following example shows how to configure FreeMarker as a view technology: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -56,7 +56,7 @@ The following example shows how to configure FreeMarker as a view technology: @Override public void configureViewResolvers(ViewResolverRegistry registry) { - registry.freemarker(); + registry.freeMarker(); } // Configure FreeMarker... @@ -69,6 +69,25 @@ The following example shows how to configure FreeMarker as a view technology: } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.freeMarker() + } + + // Configure FreeMarker... + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("classpath:/templates/freemarker") + } + } +---- Your templates need to be stored in the directory specified by the `FreeMarkerConfigurer`, shown in the preceding example. Given the preceding configuration, if your controller @@ -87,8 +106,8 @@ properties on the `FreeMarkerConfigurer` bean. The `freemarkerSettings` property a `java.util.Properties` object, and the `freemarkerVariables` property requires a `java.util.Map`. The following example shows how to use a `FreeMarkerConfigurer`: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -108,6 +127,22 @@ a `java.util.Properties` object, and the `freemarkerVariables` property requires } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + // ... + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("classpath:/templates") + setFreemarkerVariables(mapOf("xml_escape" to XmlEscape())) + } + } +---- See the FreeMarker documentation for details of settings and variables as they apply to the `Configuration` object. @@ -210,8 +245,8 @@ You can declare a `ScriptTemplateConfigurer` bean to specify the script engine t the script files to load, what function to call to render templates, and so on. The following example uses Mustache templates and the Nashorn JavaScript engine: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -233,6 +268,26 @@ The following example uses Mustache templates and the Nashorn JavaScript engine: } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.scriptTemplate() + } + + @Bean + fun configurer() = ScriptTemplateConfigurer().apply { + engineName = "nashorn" + setScripts("mustache.js") + renderObject = "Mustache" + renderFunction = "render" + } + } +---- The `render` function is called with the following parameters: @@ -252,11 +307,11 @@ https://en.wikipedia.org/wiki/Polyfill[polyfill] in order to emulate some browser facilities not available in the server-side script engine. The following example shows how to set a custom render function: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration - @EnableWebMvc + @EnableWebFlux public class WebConfig implements WebFluxConfigurer { @Override @@ -275,6 +330,26 @@ The following example shows how to set a custom render function: } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.scriptTemplate() + } + + @Bean + fun configurer() = ScriptTemplateConfigurer().apply { + engineName = "nashorn" + setScripts("polyfill.js", "handlebars.js", "render.js") + renderFunction = "render" + isSharedEngine = false + } + } +---- NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe script engines with templating libraries not designed for concurrency, such as Handlebars or @@ -284,8 +359,7 @@ to https://bugs.openjdk.java.net/browse/JDK-8076099[this bug]. `polyfill.js` defines only the `window` object needed by Handlebars to run properly, as the following snippet shows: -[source,javascript,indent=0] -[subs="verbatim,quotes"] +[source,javascript,indent=0,subs="verbatim,quotes"] ---- var window = {}; ---- @@ -296,8 +370,7 @@ This can be done on the script side, as well as any customization you need (mana template engine configuration for example). The following example shows how compile a template: -[source,javascript,indent=0] -[subs="verbatim,quotes"] +[source,javascript,indent=0,subs="verbatim,quotes"] ---- function render(template, model) { var compiledTemplate = Handlebars.compile(template); diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index f45c0dadd6..44f50245c6 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -38,8 +38,8 @@ You can also use `WebClient.builder()` with further options: The following example configures <>: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- ExchangeStrategies strategies = ExchangeStrategies.builder() .codecs(configurer -> { @@ -51,12 +51,25 @@ The following example configures < ...); @@ -85,6 +111,15 @@ To customize Reactor Netty settings, simple provide a pre-configured `HttpClient .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val httpClient = HttpClient.create().secure { ... } + + val webClient = WebClient.builder() + .clientConnector(ReactorClientHttpConnector(httpClient)) + .build() +---- [[webflux-client-builder-reactor-resources]] @@ -102,26 +137,32 @@ application deployed as a WAR), you can declare a Spring-managed bean of type Netty global resources are shut down when the Spring `ApplicationContext` is closed, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Bean public ReactorResourceFactory reactorResourceFactory() { return new ReactorResourceFactory(); } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Bean + fun reactorResourceFactory() = ReactorResourceFactory() +---- You can also choose not to participate in the global Reactor Netty resources. However, in this mode, the burden is on you to ensure that all Reactor Netty client and server instances use shared resources, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Bean public ReactorResourceFactory resourceFactory() { ReactorResourceFactory factory = new ReactorResourceFactory(); - factory.setGlobalResources(false); <1> + factory.setUseGlobalResources(false); // <1> return factory; } @@ -133,9 +174,33 @@ instances use shared resources, as the following example shows: }; ClientHttpConnector connector = - new ReactorClientHttpConnector(resourceFactory(), mapper); <2> + new ReactorClientHttpConnector(resourceFactory(), mapper); // <2> - return WebClient.builder().clientConnector(connector).build(); <3> + return WebClient.builder().clientConnector(connector).build(); // <3> + } +---- +<1> Create resources independent of global ones. +<2> Use the `ReactorClientHttpConnector` constructor with resource factory. +<3> Plug the connector into the `WebClient.Builder`. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Bean + fun resourceFactory() = ReactorResourceFactory().apply { + isUseGlobalResources = false // <1> + } + + @Bean + fun webClient(): WebClient { + + val mapper: (HttpClient) -> HttpClient = { + // Further customizations... + } + + val connector = ReactorClientHttpConnector(resourceFactory(), mapper) // <2> + + return WebClient.builder().clientConnector(connector).build() // <3> } ---- <1> Create resources independent of global ones. @@ -148,29 +213,50 @@ instances use shared resources, as the following example shows: To configure a connection timeout: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -import io.netty.channel.ChannelOption; + import io.netty.channel.ChannelOption; -HttpClient httpClient = HttpClient.create() - .tcpConfiguration(client -> - client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)); + HttpClient httpClient = HttpClient.create() + .tcpConfiguration(client -> + client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import io.netty.channel.ChannelOption + + val httpClient = HttpClient.create() + .tcpConfiguration { it.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)} ---- To configure a read and/or write timeout values: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -import io.netty.handler.timeout.ReadTimeoutHandler; -import io.netty.handler.timeout.WriteTimeoutHandler; + import io.netty.handler.timeout.ReadTimeoutHandler; + import io.netty.handler.timeout.WriteTimeoutHandler; -HttpClient httpClient = HttpClient.create() - .tcpConfiguration(client -> - client.doOnConnected(conn -> conn - .addHandlerLast(new ReadTimeoutHandler(10)) - .addHandlerLast(new WriteTimeoutHandler(10)))); + HttpClient httpClient = HttpClient.create() + .tcpConfiguration(client -> + client.doOnConnected(conn -> conn + .addHandlerLast(new ReadTimeoutHandler(10)) + .addHandlerLast(new WriteTimeoutHandler(10)))); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import io.netty.handler.timeout.ReadTimeoutHandler + import io.netty.handler.timeout.WriteTimeoutHandler + + val httpClient = HttpClient.create().tcpConfiguration { + it.doOnConnected { conn -> conn + .addHandlerLast(ReadTimeoutHandler(10)) + .addHandlerLast(WriteTimeoutHandler(10)) + } + } ---- @@ -180,8 +266,8 @@ HttpClient httpClient = HttpClient.create() The following example shows how to customize Jetty `HttpClient` settings: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- HttpClient httpClient = new HttpClient(); httpClient.setCookieStore(...); @@ -189,6 +275,15 @@ The following example shows how to customize Jetty `HttpClient` settings: WebClient webClient = WebClient.builder().clientConnector(connector).build(); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val httpClient = HttpClient() + httpClient.cookieStore = ... + val connector = JettyClientHttpConnector(httpClient) + + val webClient = WebClient.builder().clientConnector(connector).build(); +---- By default, `HttpClient` creates its own resources (`Executor`, `ByteBufferPool`, `Scheduler`), which remain active until the process exits or `stop()` is called. @@ -198,8 +293,8 @@ ensure that the resources are shut down when the Spring `ApplicationContext` is declaring a Spring-managed bean of type `JettyResourceFactory`, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Bean public JettyResourceFactory resourceFactory() { @@ -209,12 +304,11 @@ shows: @Bean public WebClient webClient() { - Consumer customizer = client -> { - // Further customizations... - }; - + HttpClient httpClient = new HttpClient(); + // Further customizations... + ClientHttpConnector connector = - new JettyClientHttpConnector(resourceFactory(), customizer); <1> + new JettyClientHttpConnector(httpClient, resourceFactory()); <1> return WebClient.builder().clientConnector(connector).build(); <2> } @@ -222,7 +316,25 @@ shows: <1> Use the `JettyClientHttpConnector` constructor with resource factory. <2> Plug the connector into the `WebClient.Builder`. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Bean + fun resourceFactory() = JettyResourceFactory() + @Bean + fun webClient(): WebClient { + + val httpClient = HttpClient() + // Further customizations... + + val connector = JettyClientHttpConnector(httpClient, resourceFactory()) // <1> + + return WebClient.builder().clientConnector(connector).build() // <2> + } +---- +<1> Use the `JettyClientHttpConnector` constructor with resource factory. +<2> Plug the connector into the `WebClient.Builder`. [[webflux-client-retrieve]] @@ -231,8 +343,8 @@ shows: The `retrieve()` method is the easiest way to get a response body and decode it. The following example shows how to do so: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- WebClient client = WebClient.create("https://example.org"); @@ -241,17 +353,35 @@ The following example shows how to do so: .retrieve() .bodyToMono(Person.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val client = WebClient.create("https://example.org") + + val result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .retrieve() + .awaitBody() +---- You can also get a stream of objects decoded from the response, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- Flux result = client.get() .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) .retrieve() .bodyToFlux(Quote.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val result = client.get() + .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) + .retrieve() + .bodyToFlow() +---- By default, responses with 4xx or 5xx status codes result in an `WebClientResponseException` or one of its HTTP status specific sub-classes, such as @@ -259,8 +389,8 @@ By default, responses with 4xx or 5xx status codes result in an You can also use the `onStatus` method to customize the resulting exception, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- Mono result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) @@ -269,6 +399,16 @@ as the following example shows: .onStatus(HttpStatus::is5xxServerError, response -> ...) .bodyToMono(Person.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .retrieve() + .onStatus(HttpStatus::is4xxClientError) { ... } + .onStatus(HttpStatus::is5xxServerError) { ... } + .awaitBody() +---- When `onStatus` is used, if the response is expected to have content, then the `onStatus` callback should consume it. If not, the content will be automatically drained to ensure @@ -283,25 +423,41 @@ resources are released. The `exchange()` method provides more control than the `retrieve` method. The following example is equivalent to `retrieve()` but also provides access to the `ClientResponse`: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- Mono result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .exchange() .flatMap(response -> response.bodyToMono(Person.class)); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody() +---- At this level, you can also create a full `ResponseEntity`: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- Mono> result = client.get() .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) .exchange() .flatMap(response -> response.toEntity(Person.class)); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .toEntity() +---- Note that (unlike `retrieve()`), with `exchange()`, there are no automatic error signals for 4xx and 5xx responses. You have to check the status code and decide how to proceed. @@ -319,10 +475,10 @@ is closed and is not placed back in the pool. == Request Body The request body can be encoded from any asynchronous type handled by `ReactiveAdapterRegistry`, -like `Mono` as the following example shows: +like `Mono` or Kotlin Coroutines `Deferred` as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- Mono personMono = ... ; @@ -333,11 +489,23 @@ like `Mono` as the following example shows: .retrieve() .bodyToMono(Void.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val personDeferred: Deferred = ... + + client.post() + .uri("/persons/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .bodyWithType(personDeferred) + .retrieve() + .awaitBody() +---- You can also have a stream of objects be encoded, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- Flux personFlux = ... ; @@ -348,22 +516,46 @@ You can also have a stream of objects be encoded, as the following example shows .retrieve() .bodyToMono(Void.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val people: Flow = ... -Alternatively, if you have the actual value, you can use the `body` shortcut method, + client.post() + .uri("/persons/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .bodyWithType(people) + .retrieve() + .awaitBody() +---- + +Alternatively, if you have the actual value, you can use the `bodyValue` shortcut method, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- Person person = ... ; Mono result = client.post() .uri("/persons/{id}", id) .contentType(MediaType.APPLICATION_JSON) - .body(person) + .bodyValue(person) .retrieve() .bodyToMono(Void.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val person: Person = ... + + client.post() + .uri("/persons/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(person) + .retrieve() + .awaitBody() +---- @@ -374,22 +566,33 @@ To send form data, you can provide a `MultiValueMap` as the body content is automatically set to `application/x-www-form-urlencoded` by the `FormHttpMessageWriter`. The following example shows how to use `MultiValueMap`: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- MultiValueMap formData = ... ; Mono result = client.post() .uri("/path", id) - .body(formData) + .bodyValue(formData) .retrieve() .bodyToMono(Void.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val formData: MultiValueMap = ... + + client.post() + .uri("/path", id) + .bodyValue(formData) + .retrieve() + .awaitBody() +---- You can also supply form data in-line by using `BodyInserters`, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- import static org.springframework.web.reactive.function.BodyInserters.*; @@ -399,6 +602,17 @@ You can also supply form data in-line by using `BodyInserters`, as the following .retrieve() .bodyToMono(Void.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import org.springframework.web.reactive.function.BodyInserters.* + + client.post() + .uri("/path", id) + .body(fromFormData("k1", "v1").with("k2", "v2")) + .retrieve() + .awaitBody() +---- @@ -410,8 +624,8 @@ either `Object` instances that represent part content or `HttpEntity` instances headers for a part. `MultipartBodyBuilder` provides a convenient API to prepare a multipart request. The following example shows how to create a `MultiValueMap`: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- MultipartBodyBuilder builder = new MultipartBodyBuilder(); builder.part("fieldPart", "fieldValue"); @@ -421,6 +635,18 @@ multipart request. The following example shows how to create a `MultiValueMap> parts = builder.build(); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val builder = MultipartBodyBuilder().apply { + part("fieldPart", "fieldValue") + part("filePart1", new FileSystemResource("...logo.png")) + part("jsonPart", new Person("Jason")) + part("myPart", part) // Part from a server request + } + + val parts = builder.build() +---- In most cases, you do not have to specify the `Content-Type` for each part. The content type is determined automatically based on the `HttpMessageWriter` chosen to serialize it @@ -431,8 +657,8 @@ builder `part` methods. Once a `MultiValueMap` is prepared, the easiest way to pass it to the the `WebClient` is through the `body` method, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- MultipartBodyBuilder builder = ...; @@ -442,6 +668,17 @@ through the `body` method, as the following example shows: .retrieve() .bodyToMono(Void.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val builder: MultipartBodyBuilder = ... + + client.post() + .uri("/path", id) + .body(builder.build()) + .retrieve() + .awaitBody() +---- If the `MultiValueMap` contains at least one non-`String` value, which could also represent regular form data (that is, `application/x-www-form-urlencoded`), you need not @@ -451,8 +688,8 @@ set the `Content-Type` to `multipart/form-data`. This is always the case when us As an alternative to `MultipartBodyBuilder`, you can also provide multipart content, inline-style, through the built-in `BodyInserters`, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- import static org.springframework.web.reactive.function.BodyInserters.*; @@ -462,7 +699,17 @@ inline-style, through the built-in `BodyInserters`, as the following example sho .retrieve() .bodyToMono(Void.class); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import org.springframework.web.reactive.function.BodyInserters.* + client.post() + .uri("/path", id) + .body(fromMultipartData("fieldPart", "value").with("filePart", resource)) + .retrieve() + .awaitBody() +---- @@ -472,74 +719,115 @@ inline-style, through the built-in `BodyInserters`, as the following example sho You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder` in order to intercept and modify requests, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -WebClient client = WebClient.builder() - .filter((request, next) -> { + WebClient client = WebClient.builder() + .filter((request, next) -> { - ClientRequest filtered = ClientRequest.from(request) - .header("foo", "bar") - .build(); + ClientRequest filtered = ClientRequest.from(request) + .header("foo", "bar") + .build(); - return next.exchange(filtered); - }) - .build(); + return next.exchange(filtered); + }) + .build(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val client = WebClient.builder() + .filter { request, next -> + + val filtered = ClientRequest.from(request) + .header("foo", "bar") + .build() + + next.exchange(filtered) + } + .build() ---- This can be used for cross-cutting concerns, such as authentication. The following example uses a filter for basic authentication through a static factory method: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- + import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -// static import of ExchangeFilterFunctions.basicAuthentication + WebClient client = WebClient.builder() + .filter(basicAuthentication("user", "password")) + .build(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication -WebClient client = WebClient.builder() - .filter(basicAuthentication("user", "password")) - .build(); + val client = WebClient.builder() + .filter(basicAuthentication("user", "password")) + .build() ---- Filters apply globally to every request. To change a filter's behavior for a specific request, you can add request attributes to the `ClientRequest` that can then be accessed by all filters in the chain, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -WebClient client = WebClient.builder() - .filter((request, next) -> { - Optional usr = request.attribute("myAttribute"); + WebClient client = WebClient.builder() + .filter((request, next) -> { + Optional usr = request.attribute("myAttribute"); + // ... + }) + .build(); + + client.get().uri("https://example.org/") + .attribute("myAttribute", "...") + .retrieve() + .bodyToMono(Void.class); + + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val client = WebClient.builder() + .filter { request, _ -> + val usr = request.attributes()["myAttribute"]; // ... - }) - .build(); + }.build() -client.get().uri("https://example.org/") - .attribute("myAttribute", "...") - .retrieve() - .bodyToMono(Void.class); - - } + client.get().uri("https://example.org/") + .attribute("myAttribute", "...") + .retrieve() + .awaitBody() ---- You can also replicate an existing `WebClient`, insert new filters, or remove already registered filters. The following example, inserts a basic authentication filter at index 0: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- + import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; -// static import of ExchangeFilterFunctions.basicAuthentication - -WebClient client = webClient.mutate() - .filters(filterList -> { - filterList.add(0, basicAuthentication("user", "password")); - }) - .build(); + WebClient client = webClient.mutate() + .filters(filterList -> { + filterList.add(0, basicAuthentication("user", "password")); + }) + .build(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val client = webClient.mutate() + .filters { it.add(0, basicAuthentication("user", "password")) } + .build() ---- - @@ -548,38 +836,69 @@ WebClient client = webClient.mutate() `WebClient` can be used in synchronous style by blocking at the end for the result: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -Person person = client.get().uri("/person/{id}", i).retrieve() - .bodyToMono(Person.class) - .block(); + Person person = client.get().uri("/person/{id}", i).retrieve() + .bodyToMono(Person.class) + .block(); -List persons = client.get().uri("/persons").retrieve() - .bodyToFlux(Person.class) - .collectList() - .block(); + List persons = client.get().uri("/persons").retrieve() + .bodyToFlux(Person.class) + .collectList() + .block(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val person = runBlocking { + client.get().uri("/person/{id}", i).retrieve() + .awaitBody() + } + + val persons = runBlocking { + client.get().uri("/persons").retrieve() + .bodyToFlow() + .toList() + } ---- However if multiple calls need to be made, it's more efficient to avoid blocking on each response individually, and instead wait for the combined result: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -Mono personMono = client.get().uri("/person/{id}", personId) - .retrieve().bodyToMono(Person.class); + Mono personMono = client.get().uri("/person/{id}", personId) + .retrieve().bodyToMono(Person.class); -Mono> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId) - .retrieve().bodyToFlux(Hobby.class).collectList(); + Mono> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId) + .retrieve().bodyToFlux(Hobby.class).collectList(); -Map data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> { - Map map = new LinkedHashMap<>(); - map.put("person", personName); - map.put("hobbies", hobbies); - return map; - }) - .block(); + Map data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> { + Map map = new LinkedHashMap<>(); + map.put("person", person); + map.put("hobbies", hobbies); + return map; + }) + .block(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val data = runBlocking { + val personDeferred = async { + client.get().uri("/person/{id}", personId) + .retrieve().awaitBody() + } + + val hobbiesDeferred = async { + client.get().uri("/person/{id}/hobbies", personId) + .retrieve().bodyToFlow().toList() + } + + mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await()) + } ---- The above is merely one example. There are lots of other patterns and operators for putting @@ -588,8 +907,10 @@ inter-dependent, without ever blocking until the end. [NOTE] ==== -You should never have to block in a Spring MVC controller. Simply return the resulting -`Flux` or `Mono` from the controller method. +With `Flux` or `Mono`, you should never have to block in a Spring MVC or Spring WebFlux controller. +Simply return the resulting reactive type from the controller method. The same principle apply to +Kotlin Coroutines and Spring WebFlux, just use suspending function or return `Flow` in your +controller method . ==== diff --git a/src/docs/asciidoc/web/webflux-websocket.adoc b/src/docs/asciidoc/web/webflux-websocket.adoc index 4585333b31..435e891fff 100644 --- a/src/docs/asciidoc/web/webflux-websocket.adoc +++ b/src/docs/asciidoc/web/webflux-websocket.adoc @@ -26,8 +26,8 @@ server-side applications that handle WebSocket messages. To create a WebSocket server, you can first create a `WebSocketHandler`. The following example shows how to do so: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- import org.springframework.web.reactive.socket.WebSocketHandler; import org.springframework.web.reactive.socket.WebSocketSession; @@ -40,14 +40,27 @@ The following example shows how to do so: } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import org.springframework.web.reactive.socket.WebSocketHandler + import org.springframework.web.reactive.socket.WebSocketSession + + class MyWebSocketHandler : WebSocketHandler { + + override fun handle(session: WebSocketSession): Mono { + // ... + } + } +---- Then you can map it to a URL and add a `WebSocketHandlerAdapter`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration - static class WebConfig { + class WebConfig { @Bean public HandlerMapping handlerMapping() { @@ -64,6 +77,24 @@ Then you can map it to a URL and add a `WebSocketHandlerAdapter`, as the followi } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + class WebConfig { + + @Bean + fun handlerMapping(): HandlerMapping { + val map = mapOf("/path" to MyWebSocketHandler()) + val order = -1 // before annotated controllers + + return SimpleUrlHandlerMapping(map, order) + } + + @Bean + fun handlerAdapter() = WebSocketHandlerAdapter() + } +---- @@ -104,23 +135,45 @@ receives a cancellation signal. The most basic implementation of a handler is one that handles the inbound stream. The following example shows such an implementation: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -class ExampleHandler implements WebSocketHandler { + class ExampleHandler implements WebSocketHandler { - @Override - public Mono handle(WebSocketSession session) { - return session.receive() <1> - .doOnNext(message -> { - // ... <2> - }) - .concatMap(message -> { - // ... <3> - }) - .then(); <4> + @Override + public Mono handle(WebSocketSession session) { + return session.receive() // <1> + .doOnNext(message -> { + // ... // <2> + }) + .concatMap(message -> { + // ... // <3> + }) + .then(); // <4> + } + } +---- +<1> Access the stream of inbound messages. +<2> Do something with each message. +<3> Perform nested asynchronous operations that use the message content. +<4> Return a `Mono` that completes when receiving completes. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + class ExampleHandler : WebSocketHandler { + + override fun handle(session: WebSocketSession): Mono { + return session.receive() // <1> + .doOnNext { + // ... // <2> + } + .concatMap { + // ... // <3> + } + .then() // <4> + } } -} ---- <1> Access the stream of inbound messages. <2> Do something with each message. @@ -135,26 +188,50 @@ released before you have had a chance to read the data. For more background, see The following implementation combines the inbound and outbound streams: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -class ExampleHandler implements WebSocketHandler { + class ExampleHandler implements WebSocketHandler { - @Override - public Mono handle(WebSocketSession session) { + @Override + public Mono handle(WebSocketSession session) { - Flux output = session.receive() <1> - .doOnNext(message -> { - // ... - }) - .concatMap(message -> { - // ... - }) - .map(value -> session.textMessage("Echo " + value)); <2> + Flux output = session.receive() // <1> + .doOnNext(message -> { + // ... + }) + .concatMap(message -> { + // ... + }) + .map(value -> session.textMessage("Echo " + value)); // <2> - return session.send(output); <3> + return session.send(output); // <3> + } + } +---- +<1> Handle the inbound message stream. +<2> Create the outbound message, producing a combined flow. +<3> Return a `Mono` that does not complete while we continue to receive. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + class ExampleHandler : WebSocketHandler { + + override fun handle(session: WebSocketSession): Mono { + + val output = session.receive() // <1> + .doOnNext { + // ... + } + .concatMap { + // ... + } + .map { session.textMessage("Echo $it") } // <2> + + return session.send(output) // <3> + } } -} ---- <1> Handle the inbound message stream. <2> Create the outbound message, producing a combined flow. @@ -164,29 +241,56 @@ class ExampleHandler implements WebSocketHandler { Inbound and outbound streams can be independent and be joined only for completion, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -class ExampleHandler implements WebSocketHandler { + class ExampleHandler implements WebSocketHandler { - @Override - public Mono handle(WebSocketSession session) { + @Override + public Mono handle(WebSocketSession session) { - Mono input = session.receive() <1> - .doOnNext(message -> { - // ... - }) - .concatMap(message -> { - // ... - }) - .then(); + Mono input = session.receive() <1> + .doOnNext(message -> { + // ... + }) + .concatMap(message -> { + // ... + }) + .then(); - Flux source = ... ; - Mono output = session.send(source.map(session::textMessage)); <2> + Flux source = ... ; + Mono output = session.send(source.map(session::textMessage)); <2> - return Mono.zip(input, output).then(); <3> + return Mono.zip(input, output).then(); <3> + } + } +---- +<1> Handle inbound message stream. +<2> Send outgoing messages. +<3> Join the streams and return a `Mono` that completes when either stream ends. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + class ExampleHandler : WebSocketHandler { + + override fun handle(session: WebSocketSession): Mono { + + val input = session.receive() // <1> + .doOnNext { + // ... + } + .concatMap { + // ... + } + .then() + + val source: Flux = ... + val output = session.send(source.map(session::textMessage)) // <2> + + return Mono.zip(input, output).then() // <3> + } } -} ---- <1> Handle inbound message stream. <2> Send outgoing messages. @@ -233,11 +337,11 @@ The `RequestUpgradeStrategy` for each server exposes WebSocket-related configura options available for the underlying WebSocket engine. The following example sets WebSocket options when running on Tomcat: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration - static class WebConfig { + class WebConfig { @Bean public WebSocketHandlerAdapter handlerAdapter() { @@ -252,6 +356,25 @@ WebSocket options when running on Tomcat: } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + class WebConfig { + + @Bean + fun handlerAdapter() = + WebSocketHandlerAdapter(webSocketService()) + + @Bean + fun webSocketService(): WebSocketService { + val strategy = TomcatRequestUpgradeStrategy().apply { + setMaxSessionIdleTimeout(0L) + } + return HandshakeWebSocketService(strategy) + } + } +---- Check the upgrade strategy for your server to see what options are available. Currently, only Tomcat and Jetty expose such options. @@ -284,16 +407,28 @@ API to suspend receiving messages for back pressure. To start a WebSocket session, you can create an instance of the client and use its `execute` methods: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -WebSocketClient client = new ReactorNettyWebSocketClient(); + WebSocketClient client = new ReactorNettyWebSocketClient(); -URI url = new URI("ws://localhost:8080/path"); -client.execute(url, session -> - session.receive() - .doOnNext(System.out::println) - .then()); + URI url = new URI("ws://localhost:8080/path"); + client.execute(url, session -> + session.receive() + .doOnNext(System.out::println) + .then()); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val client = ReactorNettyWebSocketClient() + + val url = URI("ws://localhost:8080/path") + client.execute(url) { session -> + session.receive() + .doOnNext(::println) + .then() + } ---- Some clients, such as Jetty, implement `Lifecycle` and need to be stopped and started diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc index fb095f6ead..3af5baa28f 100644 --- a/src/docs/asciidoc/web/webflux.adoc +++ b/src/docs/asciidoc/web/webflux.adoc @@ -101,6 +101,10 @@ operations on the output, but you need to adapt the output for use with another Whenever feasible (for example, annotated controllers), WebFlux adapts transparently to the use of RxJava or another reactive library. See <> for more details. +NOTE: In addition to Reactive APIs, WebFlux can also be used with +<> APIs in Kotlin which provides a more imperative style of programming. +The following Kotlin code samples will be provided with Coroutines APIs. + [[webflux-programming-models]] @@ -378,58 +382,106 @@ https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-the-Spr The code snippets below show using the `HttpHandler` adapters with each server API: *Reactor Netty* -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -HttpHandler handler = ... -ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); -HttpServer.create(host, port).newHandler(adapter).block(); + HttpHandler handler = ... + ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); + HttpServer.create().host(host).port(port).handle(adapter).bind().block(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val handler: HttpHandler = ... + val adapter = ReactorHttpHandlerAdapter(handler) + HttpServer.create().host(host).port(port).handle(adapter).bind().block() ---- *Undertow* -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -HttpHandler handler = ... -UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler); -Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build(); -server.start(); + HttpHandler handler = ... + UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler); + Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build(); + server.start(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val handler: HttpHandler = ... + val adapter = UndertowHttpHandlerAdapter(handler) + val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build() + server.start() ---- *Tomcat* -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -HttpHandler handler = ... -Servlet servlet = new TomcatHttpHandlerAdapter(handler); + HttpHandler handler = ... + Servlet servlet = new TomcatHttpHandlerAdapter(handler); -Tomcat server = new Tomcat(); -File base = new File(System.getProperty("java.io.tmpdir")); -Context rootContext = server.addContext("", base.getAbsolutePath()); -Tomcat.addServlet(rootContext, "main", servlet); -rootContext.addServletMappingDecoded("/", "main"); -server.setHost(host); -server.setPort(port); -server.start(); + Tomcat server = new Tomcat(); + File base = new File(System.getProperty("java.io.tmpdir")); + Context rootContext = server.addContext("", base.getAbsolutePath()); + Tomcat.addServlet(rootContext, "main", servlet); + rootContext.addServletMappingDecoded("/", "main"); + server.setHost(host); + server.setPort(port); + server.start(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val handler: HttpHandler = ... + val servlet = TomcatHttpHandlerAdapter(handler) + + val server = Tomcat() + val base = File(System.getProperty("java.io.tmpdir")) + val rootContext = server.addContext("", base.absolutePath) + Tomcat.addServlet(rootContext, "main", servlet) + rootContext.addServletMappingDecoded("/", "main") + server.host = host + server.setPort(port) + server.start() ---- *Jetty* -[source,java,indent=0] -[subs="verbatim,quotes"] + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -HttpHandler handler = ... -Servlet servlet = new JettyHttpHandlerAdapter(handler); + HttpHandler handler = ... + Servlet servlet = new JettyHttpHandlerAdapter(handler); -Server server = new Server(); -ServletContextHandler contextHandler = new ServletContextHandler(server, ""); -contextHandler.addServlet(new ServletHolder(servlet), "/"); -contextHandler.start(); + Server server = new Server(); + ServletContextHandler contextHandler = new ServletContextHandler(server, ""); + contextHandler.addServlet(new ServletHolder(servlet), "/"); + contextHandler.start(); -ServerConnector connector = new ServerConnector(server); -connector.setHost(host); -connector.setPort(port); -server.addConnector(connector); -server.start(); + ServerConnector connector = new ServerConnector(server); + connector.setHost(host); + connector.setPort(port); + server.addConnector(connector); + server.start(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val handler: HttpHandler = ... + val servlet = JettyHttpHandlerAdapter(handler) + + val server = Server() + val contextHandler = ServletContextHandler(server, "") + contextHandler.addServlet(ServletHolder(servlet), "/") + contextHandler.start(); + + val connector = ServerConnector(server) + connector.host = host + connector.port = port + server.addConnector(connector) + server.start() ---- *Servlet 3.1+ Container* @@ -523,11 +575,17 @@ Spring ApplicationContext, or that can be registered directly with it: `ServerWebExchange` exposes the following method for access to form data: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -Mono> getFormData(); + Mono> getFormData(); ---- +[source,Kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + suspend fun getFormData(): MultiValueMap +---- + The `DefaultServerWebExchange` uses the configured `HttpMessageReader` to parse form data (`application/x-www-form-urlencoded`) into a `MultiValueMap`. By default, @@ -541,10 +599,15 @@ The `DefaultServerWebExchange` uses the configured `HttpMessageReader` to parse `ServerWebExchange` exposes the following method for access to multipart data: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -Mono> getMultipartData(); + Mono> getMultipartData(); +---- +[source,Kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + suspend fun getMultipartData(): MultiValueMap ---- The `DefaultServerWebExchange` uses the configured @@ -826,31 +889,52 @@ headers are masked by default and you must explicitly enable their logging in fu The following example shows how to do so for server-side requests: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -@Configuration -@EnableWebFlux -class MyConfig implements WebFluxConfigurer { + @Configuration + @EnableWebFlux + class MyConfig implements WebFluxConfigurer { - @Override - public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { - configurer.defaultCodecs().enableLoggingRequestDetails(true); + @Override + public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + configurer.defaultCodecs().enableLoggingRequestDetails(true); + } + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class MyConfig : WebFluxConfigurer { + + override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { + configurer.defaultCodecs().enableLoggingRequestDetails(true) + } } -} ---- The following example shows how to do so for client-side requests: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -Consumer consumer = configurer -> - configurer.defaultCodecs().enableLoggingRequestDetails(true); + Consumer consumer = configurer -> + configurer.defaultCodecs().enableLoggingRequestDetails(true); -WebClient webClient = WebClient.builder() - .exchangeStrategies(ExchangeStrategies.builder().codecs(consumer).build()) - .build(); + WebClient webClient = WebClient.builder() + .exchangeStrategies(ExchangeStrategies.builder().codecs(consumer).build()) + .build(); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) } + + val webClient = WebClient.builder() + .exchangeStrategies(ExchangeStrategies.builder().codecs(consumer).build()) + .build() ---- @@ -882,11 +966,17 @@ Spring configuration in a WebFlux application typically contains: The configuration is given to `WebHttpHandlerBuilder` to build the processing chain, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -ApplicationContext context = ... -HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context); + ApplicationContext context = ... + HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context); +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + val context: ApplicationContext = ... + val handler = WebHttpHandlerBuilder.applicationContext(context) ---- The resulting `HttpHandler` is ready for use with a <>. @@ -1114,8 +1204,8 @@ do not have to extend base classes nor implement specific interfaces. The following listing shows a basic example: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @RestController public class HelloController { @@ -1126,6 +1216,16 @@ The following listing shows a basic example: } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @RestController + class HelloController { + + @GetMapping("/hello") + fun handle() = "Hello WebFlux" + } +---- In the preceding example, the method returns a `String` to be written to the response body. @@ -1144,11 +1244,11 @@ a web component. To enable auto-detection of such `@Controller` beans, you can add component scanning to your Java configuration, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration - @ComponentScan("org.example.web") <1> + @ComponentScan("org.example.web") // <1> public class WebConfig { // ... @@ -1156,6 +1256,18 @@ your Java configuration, as the following example shows: ---- <1> Scan the `org.example.web` package. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @ComponentScan("org.example.web") // <1> + class WebConfig { + + // ... + } +---- +<1> Scan the `org.example.web` package. + `@RestController` is a <> that is itself meta-annotated with `@Controller` and `@ResponseBody`, indicating a controller whose every method inherits the type-level `@ResponseBody` annotation and, therefore, writes @@ -1187,8 +1299,8 @@ using `@RequestMapping`, which, by default, matches to all HTTP methods. At the The following example uses type and method level mappings: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @RestController @RequestMapping("/persons") @@ -1206,6 +1318,25 @@ The following example uses type and method level mappings: } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @RestController + @RequestMapping("/persons") + class PersonController { + + @GetMapping("/{id}") + fun getPerson(@PathVariable id: Long): Person { + // ... + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun add(@RequestBody person: Person) { + // ... + } + } +---- [[webflux-ann-requestmapping-uri-templates]] @@ -1221,29 +1352,53 @@ You can map requests by using glob patterns and wildcards: You can also declare URI variables and access their values with `@PathVariable`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/owners/{ownerId}/pets/{petId}") public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/owners/{ownerId}/pets/{petId}") + fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet { + // ... + } +---- You can declare URI variables at the class and method levels, as the following example shows: -[source,java,intent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -@Controller -@RequestMapping("/owners/{ownerId}") <1> -public class OwnerController { + @Controller + @RequestMapping("/owners/{ownerId}") // <1> + public class OwnerController { - @GetMapping("/pets/{petId}") <2> - public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { - // ... + @GetMapping("/pets/{petId}") // <2> + public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { + // ... + } + } +---- +<1> Class-level URI mapping. +<2> Method-level URI mapping. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Controller + @RequestMapping("/owners/{ownerId}") // <1> + class OwnerController { + + @GetMapping("/pets/{petId}") // <2> + fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet { + // ... + } } -} ---- <1> Class-level URI mapping. <2> Method-level URI mapping. @@ -1266,14 +1421,22 @@ The syntax `{varName:regex}` declares a URI variable with a regular expression t syntax: `{varName:regex}`. For example, given a URL of `/spring-web-3.0.5 .jar`, the following method extracts the name, version, and file extension: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") public void handle(@PathVariable String version, @PathVariable String ext) { // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") + fun handle(@PathVariable version: String, @PathVariable ext: String) { + // ... + } +---- URI path patterns can also have embedded `${...}` placeholders that are resolved on startup through `PropertyPlaceHolderConfigurer` against local, system, environment, and other property @@ -1312,14 +1475,22 @@ sorted last instead. If two patterns are both catch-all, the longer is chosen. You can narrow the request mapping based on the `Content-Type` of the request, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping(path = "/pets", consumes = "application/json") public void addPet(@RequestBody Pet pet) { // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/pets", consumes = ["application/json"]) + fun addPet(@RequestBody pet: Pet) { + // ... + } +---- The consumes attribute also supports negation expressions -- for example, `!text/plain` means any content type other than `text/plain`. @@ -1339,8 +1510,8 @@ TIP: `MediaType` provides constants for commonly used media types -- for example You can narrow the request mapping based on the `Accept` request header and the list of content types that a controller method produces, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping(path = "/pets/{petId}", produces = "application/json") @ResponseBody @@ -1348,6 +1519,15 @@ content types that a controller method produces, as the following example shows: // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/pets/{petId}", produces = ["application/json"]) + @ResponseBody + fun getPet(@PathVariable String petId): Pet { + // ... + } +---- The media type can specify a character set. Negated expressions are supported -- for example, `!text/plain` means any content type other than `text/plain`. @@ -1368,29 +1548,48 @@ You can narrow request mappings based on query parameter conditions. You can tes presence of a query parameter (`myParam`), for its absence (`!myParam`), or for a specific value (`myParam=myValue`). The following examples tests for a parameter with a value: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- - @GetMapping(path = "/pets/{petId}", params = "myParam=myValue") <1> + @GetMapping(path = "/pets/{petId}", params = "myParam=myValue") // <1> public void findPet(@PathVariable String petId) { // ... } ---- <1> Check that `myParam` equals `myValue`. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/pets/{petId}", params = ["myParam=myValue"]) // <1> + fun findPet(@PathVariable petId: String) { + // ... + } +---- +<1> Check that `myParam` equals `myValue`. You can also use the same with request header conditions, as the follwing example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- - @GetMapping(path = "/pets", headers = "myHeader=myValue") <1> + @GetMapping(path = "/pets", headers = "myHeader=myValue") // <1> public void findPet(@PathVariable String petId) { // ... } ---- <1> Check that `myHeader` equals `myValue`. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/pets", headers = ["myHeader=myValue"]) // <1> + fun findPet(@PathVariable petId: String) { + // ... + } +---- +<1> Check that `myHeader` equals `myValue`. + [[webflux-ann-requestmapping-head-options]] @@ -1443,31 +1642,52 @@ You can programmatically register Handler methods, which can be used for dynamic registrations or for advanced cases, such as different instances of the same handler under different URLs. The following example shows how to do so: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -@Configuration -public class MyConfig { + @Configuration + public class MyConfig { - @Autowired - public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) <1> - throws NoSuchMethodException { + @Autowired + public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) // <1> + throws NoSuchMethodException { - RequestMappingInfo info = RequestMappingInfo - .paths("/user/{id}").methods(RequestMethod.GET).build(); <2> + RequestMappingInfo info = RequestMappingInfo + .paths("/user/{id}").methods(RequestMethod.GET).build(); // <2> - Method method = UserHandler.class.getMethod("getUser", Long.class); <3> + Method method = UserHandler.class.getMethod("getUser", Long.class); // <3> + + mapping.registerMapping(info, handler, method); // <4> + } - mapping.registerMapping(info, handler, method); <4> } - -} ---- <1> Inject target handlers and the handler mapping for controllers. <2> Prepare the request mapping metadata. <3> Get the handler method. <4> Add the registration. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + class MyConfig { + + @Autowired + fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { // <1> + + val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() // <2> + + val method = UserHandler::class.java.getMethod("getUser", Long::class.java) // <3> + + mapping.registerMapping(info, handler, method) // <4> + } + } +---- +<1> Inject target handlers and the handler mapping for controllers. +<2> Prepare the request mapping metadata. +<3> Get the handler method. +<4> Add the registration. [[webflux-ann-methods]] @@ -1707,8 +1927,8 @@ to mask variable content. That said, if you want to access matrix variables from controller method, you need to add a URI variable to the path segment where matrix variables are expected. The following example shows how to do so: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- // GET /pets/42;q=11;r=22 @@ -1719,13 +1939,26 @@ variables are expected. The following example shows how to do so: // q == 11 } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // GET /pets/42;q=11;r=22 + + @GetMapping("/pets/{petId}") + fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) { + + // petId == 42 + // q == 11 + } +---- + Given that all path segments can contain matrix variables, you may sometimes need to disambiguate which path variable the matrix variable is expected to be in, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- // GET /owners/42;q=11/pets/21;q=22 @@ -1738,12 +1971,24 @@ as the following example shows: // q2 == 22 } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/owners/{ownerId}/pets/{petId}") + fun findPet( + @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int, + @MatrixVariable(name = "q", pathVar = "petId") q2: Int) { + + // q1 == 11 + // q2 == 22 + } +---- You can define a matrix variable may be defined as optional and specify a default value as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- // GET /pets/42 @@ -1753,18 +1998,43 @@ as the following example shows: // q == 1 } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // GET /pets/42 + + @GetMapping("/pets/{petId}") + fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) { + + // q == 1 + } +---- To get all matrix variables, use a `MultiValueMap`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- // GET /owners/42;q=11;r=12/pets/21;q=22;s=23 @GetMapping("/owners/{ownerId}/pets/{petId}") public void findPet( @MatrixVariable MultiValueMap matrixVars, - @MatrixVariable(pathVar="petId"") MultiValueMap petMatrixVars) { + @MatrixVariable(pathVar="petId") MultiValueMap petMatrixVars) { + + // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] + // petMatrixVars: ["q" : 22, "s" : 23] + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // GET /owners/42;q=11;r=12/pets/21;q=22;s=23 + + @GetMapping("/owners/{ownerId}/pets/{petId}") + fun findPet( + @MatrixVariable matrixVars: MultiValueMap, + @MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap) { // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] // petMatrixVars: ["q" : 22, "s" : 23] @@ -1779,8 +2049,8 @@ To get all matrix variables, use a `MultiValueMap`, as the following example sho You can use the `@RequestParam` annotation to bind query parameters to a method argument in a controller. The following code snippet shows the usage: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Controller @RequestMapping("/pets") @@ -1796,7 +2066,29 @@ controller. The following code snippet shows the usage: } // ... + } +---- +<1> Using `@RequestParam`. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import org.springframework.ui.set + + @Controller + @RequestMapping("/pets") + class EditPetForm { + + // ... + + @GetMapping + fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { // <1> + val pet = clinic.loadPet(petId) + model["pet"] = pet + return "petForm" + } + + // ... } ---- <1> Using `@RequestParam`. @@ -1847,13 +2139,26 @@ Keep-Alive 300 The following example gets the value of the `Accept-Encoding` and `Keep-Alive` headers: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/demo") public void handle( - @RequestHeader("Accept-Encoding") String encoding, <1> - @RequestHeader("Keep-Alive") long keepAlive) { <2> + @RequestHeader("Accept-Encoding") String encoding, // <1> + @RequestHeader("Keep-Alive") long keepAlive) { // <2> + //... + } +---- +<1> Get the value of the `Accept-Encoging` header. +<2> Get the value of the `Keep-Alive` header. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/demo") + fun handle( + @RequestHeader("Accept-Encoding") encoding: String, // <1> + @RequestHeader("Keep-Alive") keepAlive: Long) { // <2> //... } ---- @@ -1882,19 +2187,28 @@ in a controller. The following example shows a request with a cookie: -[literal] -[subs="verbatim,quotes"] +[literal,subs="verbatim,quotes"] ---- JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84 ---- The following code sample demonstrates how to get the cookie value: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/demo") - public void handle(@CookieValue("JSESSIONID") String cookie) { <1> + public void handle(@CookieValue("JSESSIONID") String cookie) { // <1> + //... + } +---- +<1> Get the cookie value. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/demo") + fun handle(@CookieValue("JSESSIONID") cookie: String) { // <1> //... } ---- @@ -1915,11 +2229,19 @@ the values of query parameters and form fields whose names match to field names. referred to as data binding, and it saves you from having to deal with parsing and converting individual query parameters and form fields. The following example binds an instance of `Pet`: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") - public String processSubmit(@ModelAttribute Pet pet) { } <1> + public String processSubmit(@ModelAttribute Pet pet) { } // <1> +---- +<1> Bind an instance of `Pet`. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/owners/{ownerId}/pets/{petId}/edit") + fun processSubmit(@ModelAttribute pet: Pet): String { } // <1> ---- <1> Bind an instance of `Pet`. @@ -1943,8 +2265,8 @@ Data binding can result in errors. By default, a `WebExchangeBindException` is r to check for such errors in the controller method, you can add a `BindingResult` argument immediately next to the `@ModelAttribute`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { <1> @@ -1956,16 +2278,29 @@ immediately next to the `@ModelAttribute`, as the following example shows: ---- <1> Adding a `BindingResult`. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/owners/{ownerId}/pets/{petId}/edit") + fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> + if (result.hasErrors()) { + return "petForm" + } + // ... + } +---- +<1> Adding a `BindingResult`. + You can automatically apply validation after data binding by adding the `javax.validation.Valid` annotation or Spring's `@Validated` annotation (see also <> and <>). The following example uses the `@Valid` annotation: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") - public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { <1> + public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1> if (result.hasErrors()) { return "petForm"; } @@ -1974,6 +2309,19 @@ You can automatically apply validation after data binding by adding the ---- <1> Using `@Valid` on a model attribute argument. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/owners/{ownerId}/pets/{petId}/edit") + fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1> + if (result.hasErrors()) { + return "petForm" + } + // ... + } +---- +<1> Using `@Valid` on a model attribute argument. + Spring WebFlux, unlike Spring MVC, supports reactive types in the model -- for example, `Mono` or `io.reactivex.Single`. You can declare a `@ModelAttribute` argument with or without a reactive type wrapper, and it will be resolved accordingly, @@ -1982,8 +2330,8 @@ argument, you must declare the `@ModelAttribute` argument before it without a re type wrapper, as shown earlier. Alternatively, you can handle any errors through the reactive type, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/owners/{ownerId}/pets/{petId}/edit") public Mono processSubmit(@Valid @ModelAttribute("pet") Mono petMono) { @@ -1996,6 +2344,20 @@ reactive type, as the following example shows: }); } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/owners/{ownerId}/pets/{petId}/edit") + fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono): Mono { + return petMono + .flatMap { pet -> + // ... + } + .onErrorResume{ ex -> + // ... + } + } +---- Note that use of `@ModelAttribute` is optional -- for example, to set its attributes. By default, any argument that is not a simple value type( as determined by @@ -2016,8 +2378,8 @@ requests to access. Consider the following example: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Controller @SessionAttributes("pet") <1> @@ -2027,23 +2389,34 @@ Consider the following example: ---- <1> Using the `@SessionAttributes` annotation. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Controller + @SessionAttributes("pet") // <1> + class EditPetForm { + // ... + } +---- +<1> Using the `@SessionAttributes` annotation. + On the first request, when a model attribute with the name, `pet`, is added to the model, it is automatically promoted to and saved in the `WebSession`. It remains there until another controller method uses a `SessionStatus` method argument to clear the storage, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Controller - @SessionAttributes("pet") <1> + @SessionAttributes("pet") // <1> public class EditPetForm { // ... @PostMapping("/pets/{id}") - public String handle(Pet pet, BindingResult errors, SessionStatus status) { <2> - if (errors.hasErrors) { + public String handle(Pet pet, BindingResult errors, SessionStatus status) { // <2> + if (errors.hasErrors()) { // ... } status.setComplete(); @@ -2055,6 +2428,28 @@ as the following example shows: <1> Using the `@SessionAttributes` annotation. <2> Using a `SessionStatus` variable. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Controller + @SessionAttributes("pet") // <1> + class EditPetForm { + + // ... + + @PostMapping("/pets/{id}") + fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { // <2> + if (errors.hasErrors()) { + // ... + } + status.setComplete() + // ... + } + } +---- +<1> Using the `@SessionAttributes` annotation. +<2> Using a `SessionStatus` variable. + [[webflux-ann-sessionattribute]] ==== `@SessionAttribute` @@ -2064,11 +2459,21 @@ If you need access to pre-existing session attributes that are managed globally (that is, outside the controller -- for example, by a filter) and may or may not be present, you can use the `@SessionAttribute` annotation on a method parameter, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/") - public String handle(@SessionAttribute User user) { <1> + public String handle(@SessionAttribute User user) { // <1> + // ... + } +---- +<1> Using `@SessionAttribute`. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/") + fun handle(@SessionAttribute user: User): String { // <1> // ... } ---- @@ -2090,8 +2495,8 @@ Similarly to `@SessionAttribute`, you can use the `@RequestAttribute` annotation access pre-existing request attributes created earlier (for example, by a `WebFilter`), as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/") public String handle(@RequestAttribute Client client) { <1> @@ -2100,6 +2505,16 @@ as the following example shows: ---- <1> Using `@RequestAttribute`. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/") + fun handle(@RequestAttribute client: Client): String { // <1> + // ... + } +---- +<1> Using `@RequestAttribute`. + [[webflux-multipart-forms]] ==== Multipart Content @@ -2110,35 +2525,51 @@ content. The best way to handle a file upload form (for example, from a browser) is through data binding to a <>, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -class MyForm { + class MyForm { - private String name; + private String name; - private MultipartFile file; + private MultipartFile file; - // ... - -} - -@Controller -public class FileUploadController { - - @PostMapping("/form") - public String handleFormUpload(MyForm form, BindingResult errors) { // ... + } -} + @Controller + public class FileUploadController { + + @PostMapping("/form") + public String handleFormUpload(MyForm form, BindingResult errors) { + // ... + } + + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + class MyForm( + val name: String, + val file: MultipartFile) + + @Controller + class FileUploadController { + + @PostMapping("/form") + fun handleFormUpload(form: MyForm, errors: BindingResult): String { + // ... + } + + } ---- You can also submit multipart requests from non-browser clients in a RESTful service scenario. The following example uses a file along with JSON: -[literal] -[subs="verbatim,quotes"] +[literal,subs="verbatim,quotes"] ---- POST /someUrl Content-Type: multipart/mixed @@ -2160,26 +2591,49 @@ Content-Transfer-Encoding: 8bit You can access individual parts with `@RequestPart`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/") - public String handle(@RequestPart("meta-data") Part metadata, <1> - @RequestPart("file-data") FilePart file) { <2> + public String handle(@RequestPart("meta-data") Part metadata, // <1> + @RequestPart("file-data") FilePart file) { // <2> // ... } ---- <1> Using `@RequestPart` to get the metadata. <2> Using `@RequestPart` to get the file. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/") + fun handle(@RequestPart("meta-data") Part metadata, // <1> + @RequestPart("file-data") FilePart file): String { // <2> + // ... + } +---- +<1> Using `@RequestPart` to get the metadata. +<2> Using `@RequestPart` to get the file. + + To deserialize the raw part content (for example, to JSON -- similar to `@RequestBody`), you can declare a concrete target `Object`, instead of `Part`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/") - public String handle(@RequestPart("meta-data") MetaData metadata) { <1> + public String handle(@RequestPart("meta-data") MetaData metadata) { // <1> + // ... + } +---- +<1> Using `@RequestPart` to get the metadata. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/") + fun handle(@RequestPart("meta-data") metadata: MetaData): String { // <1> // ... } ---- @@ -2191,14 +2645,26 @@ By default, validation errors cause a `WebExchangeBindException`, which is turne into a 400 (`BAD_REQUEST`) response. Alternatively, you can handle validation errors locally within the controller through an `Errors` or `BindingResult` argument, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- -@PostMapping("/") -public String handle(@Valid @RequestPart("meta-data") MetaData metadata, <1> - BindingResult result) { <2> - // ... -} + @PostMapping("/") + public String handle(@Valid @RequestPart("meta-data") MetaData metadata, // <1> + BindingResult result) { <2> + // ... + } +---- +<1> Using a `@Valid` annotation. +<2> Using a `BindingResult` argument. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/") + fun handle(@Valid @RequestPart("meta-data") metadata: MetaData, // <1> + result: BindingResult): String { // <2> + // ... + } ---- <1> Using a `@Valid` annotation. <2> Using a `BindingResult` argument. @@ -2206,21 +2672,32 @@ public String handle(@Valid @RequestPart("meta-data") MetaData metadata, <1> To access all multipart data as a `MultiValueMap`, you can use `@RequestBody`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/") - public String handle(@RequestBody Mono> parts) { <1> + public String handle(@RequestBody Mono> parts) { // <1> // ... } ---- <1> Using `@RequestBody`. -To access multipart data sequentially, in streaming fashion, you can use `@RequestBody` with -`Flux` instead, as the following example shows: +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/") + fun handle(@RequestBody parts: MultiValueMap): String { // <1> + // ... + } +---- +<1> Using `@RequestBody`. -[source,java,indent=0] -[subs="verbatim,quotes"] + +To access multipart data sequentially, in streaming fashion, you can use `@RequestBody` with +`Flux` (or `Flow` in Kotlin) instead, as the following example shows: + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/") public String handle(@RequestBody Flux parts) { <1> @@ -2229,6 +2706,16 @@ To access multipart data sequentially, in streaming fashion, you can use `@Reque ---- <1> Using `@RequestBody`. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/") + fun handle(@RequestBody parts: Flow): String { // <1> + // ... + } +---- +<1> Using `@RequestBody`. + [[webflux-ann-requestbody]] ==== `@RequestBody` @@ -2238,8 +2725,8 @@ You can use the `@RequestBody` annotation to have the request body read and dese `Object` through an <>. The following example uses a `@RequestBody` argument: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/accounts") public void handle(@RequestBody Account account) { @@ -2247,18 +2734,34 @@ The following example uses a `@RequestBody` argument: } ---- -Unlike Spring MVC, in WebFlux, the `@RequestBody` method argument supports reactive types -and fully non-blocking reading and (client-to-server) streaming. The following example -uses a `Mono`: +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/accounts") + fun handle(@RequestBody account: Account) { + // ... + } +---- -[source,java,indent=0] -[subs="verbatim,quotes"] +Unlike Spring MVC, in WebFlux, the `@RequestBody` method argument supports reactive types +and fully non-blocking reading and (client-to-server) streaming. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/accounts") public void handle(@RequestBody Mono account) { // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/accounts") + fun handle(@RequestBody accounts: Flow) { + // ... + } +---- You can use the <> option of the <> to configure or customize message readers. @@ -2270,14 +2773,22 @@ into a 400 (`BAD_REQUEST`) response. Alternatively, you can handle validation er within the controller through an `Errors` or a `BindingResult` argument. The following example uses a `BindingResult` argument`: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/accounts") public void handle(@Valid @RequestBody Account account, BindingResult result) { // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/accounts") + fun handle(@Valid @RequestBody account: Account, result: BindingResult) { + // ... + } +---- [[webflux-ann-httpentity]] @@ -2288,14 +2799,22 @@ example uses a `BindingResult` argument`: container object that exposes request headers and the body. The following example uses an `HttpEntity`: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @PostMapping("/accounts") public void handle(HttpEntity entity) { // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @PostMapping("/accounts") + fun handle(entity: HttpEntity) { + // ... + } +---- [[webflux-ann-responsebody]] @@ -2306,8 +2825,8 @@ You can use the `@ResponseBody` annotation on a method to have the return serial to the response body through an <>. The following example shows how to do so: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/accounts/{id}") @ResponseBody @@ -2315,6 +2834,15 @@ example shows how to do so: // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/accounts/{id}") + @ResponseBody + fun handle(): Account { + // ... + } +---- `@ResponseBody` is also supported at the class level, in which case it is inherited by all controller methods. This is the effect of `@RestController`, which is nothing more @@ -2338,8 +2866,8 @@ configure or customize message writing. `ResponseEntity` is like <> but with status and headers. For example: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/something") public ResponseEntity handle() { @@ -2348,6 +2876,16 @@ configure or customize message writing. return ResponseEntity.ok().eTag(etag).build(body); } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/something") + fun handle(): ResponseEntity { + val body: String = ... + val etag: String = ... + return ResponseEntity.ok().eTag(etag).build(body) + } +---- WebFlux supports using a single value <> to produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive types @@ -2369,8 +2907,8 @@ which allows rendering only a subset of all fields in an `Object`. To use it wit `@ResponseBody` or `ResponseEntity` controller methods, you can use Jackson's `@JsonView` annotation to activate a serialization view class, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @RestController public class UserController { @@ -2410,6 +2948,28 @@ which allows rendering only a subset of all fields in an `Object`. To use it wit } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @RestController + class UserController { + + @GetMapping("/user") + @JsonView(User.WithoutPasswordView::class) + fun getUser(): User { + return User("eric", "7!jd#h23") + } + } + + class User( + @JsonView(WithoutPasswordView::class) val username: String, + @JsonView(WithPasswordView::class) val password: String + ) { + interface WithoutPasswordView + interface WithPasswordView : WithoutPasswordView + } +---- + NOTE: `@JsonView` allows an array of view classes but you can only specify only one per controller method. Use a composite interface if you need to activate multiple views. @@ -2440,8 +3000,8 @@ related to the request body). The following example uses a `@ModelAttribute` method: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @ModelAttribute public void populateModel(@RequestParam String number, Model model) { @@ -2449,17 +3009,34 @@ The following example uses a `@ModelAttribute` method: // add more ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @ModelAttribute + fun populateModel(@RequestParam number: String, model: Model) { + model.addAttribute(accountRepository.findAccount(number)) + // add more ... + } +---- The following example adds one attribute only: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @ModelAttribute public Account addAccount(@RequestParam String number) { return accountRepository.findAccount(number); } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @ModelAttribute + fun addAccount(@RequestParam number: String): Account { + return accountRepository.findAccount(number); + } +---- NOTE: When a name is not explicitly specified, a default name is chosen based on the type, as explained in the javadoc for {api-spring-framework}/core/Conventions.html[`Conventions`]. @@ -2472,8 +3049,8 @@ attributes can be transparently resolved (and the model updated) to their actual at the time of `@RequestMapping` invocation, provided a `@ModelAttribute` argument is declared without a wrapper, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @ModelAttribute public void addAccount(@RequestParam String number) { @@ -2486,6 +3063,23 @@ declared without a wrapper, as the following example shows: // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + import org.springframework.ui.set + + @ModelAttribute + fun addAccount(@RequestParam number: String) { + val accountMono: Mono = accountRepository.findAccount(number) + model["account"] = accountMono + } + + @PostMapping("/accounts") + fun handle(@ModelAttribute account: Account, errors: BindingResult): String { + // ... + } +---- + In addition, any model attributes that have a reactive type wrapper are resolved to their actual values (and the model updated) just prior to view rendering. @@ -2497,8 +3091,8 @@ controllers, unless the return value is a `String` that would otherwise be inter as a view name. `@ModelAttribute` can also help to customize the model attribute name, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/accounts/{id}") @ModelAttribute("myAccount") @@ -2507,6 +3101,16 @@ as the following example shows: return account; } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/accounts/{id}") + @ModelAttribute("myAccount") + fun handle(): Account { + // ... + return account + } +---- @@ -2532,13 +3136,13 @@ do, except for `@ModelAttribute` (command object) arguments. Typically, they are with a `WebDataBinder` argument, for registrations, and a `void` return value. The following example uses the `@InitBinder` annotation: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Controller public class FormController { - @InitBinder <1> + @InitBinder // <1> public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); dateFormat.setLenient(false); @@ -2550,12 +3154,29 @@ The following example uses the `@InitBinder` annotation: ---- <1> Using the `@InitBinder` annotation. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Controller + class FormController { + + @InitBinder // <1> + fun initBinder(binder: WebDataBinder) { + val dateFormat = SimpleDateFormat("yyyy-MM-dd") + dateFormat.isLenient = false + binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false)) + } + + // ... + } +---- + Alternatively, when using a `Formatter`-based setup through a shared `FormattingConversionService`, you could re-use the same approach and register controller-specific `Formatter` instances, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Controller public class FormController { @@ -2570,6 +3191,22 @@ controller-specific `Formatter` instances, as the following example shows: ---- <1> Adding a custom formatter (a `DateFormatter`, in this case). +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Controller + class FormController { + + @InitBinder + fun initBinder(binder: WebDataBinder) { + binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) // <1> + } + + // ... + } +---- +<1> Adding a custom formatter (a `DateFormatter`, in this case). + [[webflux-ann-controller-exceptions]] @@ -2580,21 +3217,37 @@ controller-specific `Formatter` instances, as the following example shows: `@ExceptionHandler` methods to handle exceptions from controller methods. The following example includes such a handler method: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Controller public class SimpleController { // ... - @ExceptionHandler <1> + @ExceptionHandler // <1> public ResponseEntity handle(IOException ex) { // ... } } ---- -<1> Declaring an `@ExceptionHandler`: +<1> Declaring an `@ExceptionHandler`. + +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Controller + class SimpleController { + + // ... + + @ExceptionHandler // <1> + fun handle(ex: IOException): ResponseEntity { + // ... + } + } +---- +<1> Declaring an `@ExceptionHandler`. The exception can match against a top-level exception being propagated (that is, a direct @@ -2661,8 +3314,8 @@ By default, `@ControllerAdvice` methods apply to every request (that is, all con but you can narrow that down to a subset of controllers by using attributes on the annotation, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- // Target all Controllers annotated with @RestController @ControllerAdvice(annotations = RestController.class) @@ -2677,6 +3330,22 @@ annotation, as the following example shows: public class ExampleAdvice3 {} ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // Target all Controllers annotated with @RestController + @ControllerAdvice(annotations = [RestController::class]) + public class ExampleAdvice1 {} + + // Target all Controllers within specific packages + @ControllerAdvice("org.example.controllers") + public class ExampleAdvice2 {} + + // Target all Controllers assignable to specific classes + @ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class]) + public class ExampleAdvice3 {} +---- + The selectors in the preceding example are evaluated at runtime and may negatively impact performance if used extensively. See the {api-spring-framework}/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice`] @@ -2749,8 +3418,8 @@ While https://tools.ietf.org/html/rfc7234#section-5.2.2[RFC 7234] describes all directives for the `Cache-Control` response header, the `CacheControl` type takes a use case-oriented approach that focuses on the common scenarios, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- // Cache for an hour - "Cache-Control: max-age=3600" CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS); @@ -2764,6 +3433,21 @@ use case-oriented approach that focuses on the common scenarios, as the followin CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic(); ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + // Cache for an hour - "Cache-Control: max-age=3600" + val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS) + + // Prevent caching - "Cache-Control: no-store" + val ccNoStore = CacheControl.noStore() + + // Cache for ten days in public and private caches, + // public caches should not transform the response + // "Cache-Control: max-age=864000, public, no-transform" + val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic() + +---- [[webflux-caching-etag-lastmodified]] @@ -2775,8 +3459,8 @@ Controllers can add explicit support for HTTP caching. We recommend doing so, si against conditional request headers. A controller can add an `ETag` and `Cache-Control` settings to a `ResponseEntity`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @GetMapping("/book/{id}") public ResponseEntity showBook(@PathVariable Long id) { @@ -2792,6 +3476,23 @@ settings to a `ResponseEntity`, as the following example shows: } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @GetMapping("/book/{id}") + fun showBook(@PathVariable id: Long): ResponseEntity { + + val book = findBook(id) + val version = book.getVersion() + + return ResponseEntity + .ok() + .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)) + .eTag(version) // lastModified is also available + .body(book) + } +---- + The preceding example sends a 304 (NOT_MODIFIED) response with an empty body if the comparison to the conditional request headers indicates the content has not changed. Otherwise, the `ETag` and `Cache-Control` headers are added to the response. @@ -2799,23 +3500,42 @@ to the conditional request headers indicates the content has not changed. Otherw You can also make the check against conditional request headers in the controller, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @RequestMapping public String myHandleMethod(ServerWebExchange exchange, Model model) { - long eTag = ... <1> + long eTag = ... // <1> if (exchange.checkNotModified(eTag)) { - return null; <2> + return null; // <2> } - model.addAttribute(...); <3> + model.addAttribute(...); // <3> return "myViewName"; } ---- +<1> Application-specific calculation. +<2> Response has been set to 304 (NOT_MODIFIED). No further processing. +<3> Continue with request processing. +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @RequestMapping + fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? { + + val eTag: Long = ... // <1> + + if (exchange.checkNotModified(eTag)) { + return null// <2> + } + + model.addAttribute(...) // <3> + return "myViewName" + } +---- <1> Application-specific calculation. <2> Response has been set to 304 (NOT_MODIFIED). No further processing. <3> Continue with request processing. @@ -2860,8 +3580,8 @@ gain full control over the configuration through the You can use the `@EnableWebFlux` annotation in your Java config, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -2869,6 +3589,14 @@ You can use the `@EnableWebFlux` annotation in your Java config, as the followin } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig +---- + The preceding example registers a number of Spring WebFlux <> and adapts to dependencies available on the classpath -- for JSON, XML, and others. @@ -2882,18 +3610,28 @@ available on the classpath -- for JSON, XML, and others. In your Java configuration, you can implement the `WebFluxConfigurer` interface, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux public class WebConfig implements WebFluxConfigurer { // Implement configuration methods... - } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + // Implement configuration methods... +} +---- + [[webflux-config-conversion]] @@ -2906,8 +3644,8 @@ formatting library is also installed if Joda-Time is present on the classpath. The following example shows how to register custom formatters and converters: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -2920,6 +3658,18 @@ The following example shows how to register custom formatters and converters: } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun addFormatters(registry: FormatterRegistry) { + // ... + } + } +---- NOTE: See <> and the `FormattingConversionServiceFactoryBean` for more information on when to @@ -2939,8 +3689,8 @@ is registered as a global <> for use with `@Valid In your Java configuration, you can customize the global `Validator` instance, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -2953,12 +3703,25 @@ as the following example shows: } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun getValidator(): Validator { + // ... + } + + } +---- Note that you can also register `Validator` implementations locally, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Controller public class MyController { @@ -2970,6 +3733,19 @@ as the following example shows: } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Controller + class MyController { + + @InitBinder + protected fun initBinder(binder: WebDataBinder) { + binder.addValidators(FooValidator()) + } + } +---- + TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and mark it with `@Primary` in order to avoid conflict with the one declared in the MVC config. @@ -2986,8 +3762,8 @@ but you can also enable a query parameter-based strategy. The following example shows how to customize the requested content type resolution: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -2999,6 +3775,18 @@ The following example shows how to customize the requested content type resoluti } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) { + // ... + } + } +---- @@ -3008,8 +3796,8 @@ The following example shows how to customize the requested content type resoluti The following example shows how to customize how the request and response body are read and written: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -3021,6 +3809,18 @@ The following example shows how to customize how the request and response body a } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { + // ... + } + } +---- `ServerCodecConfigurer` provides a set of default readers and writers. You can use it to add more readers and writers, customize the default ones, or replace the default ones completely. @@ -3047,8 +3847,8 @@ It also automatically registers the following well-known modules if they are det The following example shows how to configure view resolution: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -3060,13 +3860,25 @@ The following example shows how to configure view resolution: } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + // ... + } + } +---- The `ViewResolverRegistry` has shortcuts for view technologies with which the Spring Framework integrates. The following example uses FreeMarker (which also requires configuring the underlying FreeMarker view technology): -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -3088,11 +3900,30 @@ underlying FreeMarker view technology): } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.freeMarker() + } + + // Configure Freemarker... + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("classpath:/templates") + } + } +---- You can also plug in any `ViewResolver` implementation, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -3106,14 +3937,27 @@ You can also plug in any `ViewResolver` implementation, as the following example } } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + val resolver: ViewResolver = ... + registry.viewResolver(resolver + } + } +---- To support <> and rendering other formats through view resolution (besides HTML), you can configure one or more default views based on the `HttpMessageWriterView` implementation, which accepts any of the available <> from `spring-web`. The following example shows how to do so: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -3131,6 +3975,24 @@ on the `HttpMessageWriterView` implementation, which accepts any of the availabl // ... } ---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.freeMarker() + + val encoder = Jackson2JsonEncoder() + registry.defaultViews(HttpMessageWriterView(encoder)) + } + + // ... + } +---- See <> for more on the view technologies that are integrated with Spring WebFlux. @@ -3150,8 +4012,8 @@ and a reduction in HTTP requests made by the browser. The `Last-Modified` header evaluated and, if present, a `304` status code is returned. The following list shows the example: -[source,java,indent=0] -[subs="verbatim"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -3166,6 +4028,20 @@ the example: } ---- +[source,kotlin,indent=0,subs="verbatim",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)) + } + } +---- // TODO: See also <>. @@ -3181,8 +4057,8 @@ JavaScript resources used with a module loader). The following example shows how to use `VersionResourceResolver` in your Java configuration: -[source,java,indent=0] -[subs="verbatim"] +[source,java,indent=0,subs="verbatim",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -3198,6 +4074,22 @@ The following example shows how to use `VersionResourceResolver` in your Java co } ---- +[source,kotlin,indent=0,subs="verbatim",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public/") + .resourceChain(true) + .addResolver(VersionResourceResolver().addContentVersionStrategy("/**")) + } + + } +---- You can use `ResourceUrlProvider` to rewrite URLs and apply the full chain of resolvers and transformers (for example, to insert versions). The WebFlux configuration provides a `ResourceUrlProvider` @@ -3229,8 +4121,8 @@ You can customize options related to path matching. For details on the individua {api-spring-framework}/web/reactive/config/PathMatchConfigurer.html[`PathMatchConfigurer`] javadoc. The following example shows how to use `PathMatchConfigurer`: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration @EnableWebFlux @@ -3244,7 +4136,23 @@ The following example shows how to use `PathMatchConfigurer`: .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); } + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + @EnableWebFlux + class WebConfig : WebFluxConfigurer { + @Override + fun configurePathMatch(configurer: PathMatchConfigurer) { + configurer + .setUseCaseSensitiveMatch(true) + .setUseTrailingSlashMatch(false) + .addPathPrefix("/api", + HandlerTypePredicate.forAnnotation(RestController::class.java)) + } } ---- @@ -3277,14 +4185,22 @@ For advanced mode, you can remove `@EnableWebFlux` and extend directly from `DelegatingWebFluxConfiguration` instead of implementing `WebFluxConfigurer`, as the following example shows: -[source,java,indent=0] -[subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +.Java ---- @Configuration public class WebConfig extends DelegatingWebFluxConfiguration { // ... + } +---- +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +.Kotlin +---- + @Configuration + class WebConfig : DelegatingWebFluxConfiguration { + // ... } ----