Migrate Structure
This commit is contained in:
199
framework-docs/modules/ROOT/pages/web/integration.adoc
Normal file
199
framework-docs/modules/ROOT/pages/web/integration.adoc
Normal file
@@ -0,0 +1,199 @@
|
||||
[[web-integration]]
|
||||
= Other Web Frameworks
|
||||
|
||||
This chapter details Spring's integration with third-party web frameworks.
|
||||
|
||||
One of the core value propositions of the Spring Framework is that of enabling
|
||||
_choice_. In a general sense, Spring does not force you to use or buy into any
|
||||
particular architecture, technology, or methodology (although it certainly recommends
|
||||
some over others). This freedom to pick and choose the architecture, technology, or
|
||||
methodology that is most relevant to a developer and their development team is
|
||||
arguably most evident in the web area, where Spring provides its own web frameworks
|
||||
(<<mvc, Spring MVC>> and <<web-reactive.adoc#webflux, Spring WebFlux>>) while, at the same time,
|
||||
supporting integration with a number of popular third-party web frameworks.
|
||||
|
||||
|
||||
|
||||
|
||||
[[web-integration-common]]
|
||||
== Common Configuration
|
||||
|
||||
Before diving into the integration specifics of each supported web framework, let us
|
||||
first take a look at common Spring configuration that is not specific to any one web
|
||||
framework. (This section is equally applicable to Spring's own web framework variants.)
|
||||
|
||||
One of the concepts (for want of a better word) espoused by Spring's lightweight
|
||||
application model is that of a layered architecture. Remember that in a "classic"
|
||||
layered architecture, the web layer is but one of many layers. It serves as one of the
|
||||
entry points into a server-side application, and it delegates to service objects
|
||||
(facades) that are defined in a service layer to satisfy business-specific (and
|
||||
presentation-technology agnostic) use cases. In Spring, these service objects, any other
|
||||
business-specific objects, data-access objects, and others exist in a distinct "business
|
||||
context", which contains no web or presentation layer objects (presentation objects,
|
||||
such as Spring MVC controllers, are typically configured in a distinct "presentation
|
||||
context"). This section details how you can configure a Spring container (a
|
||||
`WebApplicationContext`) that contains all of the 'business beans' in your application.
|
||||
|
||||
Moving on to specifics, all you need to do is declare a
|
||||
{api-spring-framework}/web/context/ContextLoaderListener.html[`ContextLoaderListener`]
|
||||
in the standard Jakarta EE servlet `web.xml` file of your web application and add a
|
||||
`contextConfigLocation` `<context-param/>` section (in the same file) that defines which
|
||||
set of Spring XML configuration files to load.
|
||||
|
||||
Consider the following `<listener/>` configuration:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<listener>
|
||||
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
|
||||
</listener>
|
||||
----
|
||||
|
||||
Further consider the following `<context-param/>` configuration:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<context-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>/WEB-INF/applicationContext*.xml</param-value>
|
||||
</context-param>
|
||||
----
|
||||
|
||||
If you do not specify the `contextConfigLocation` context parameter, the
|
||||
`ContextLoaderListener` looks for a file called `/WEB-INF/applicationContext.xml` to
|
||||
load. Once the context files are loaded, Spring creates a
|
||||
{api-spring-framework}/web/context/WebApplicationContext.html[`WebApplicationContext`]
|
||||
object based on the bean definitions and stores it in the `ServletContext` of the web
|
||||
application.
|
||||
|
||||
All Java web frameworks are built on top of the Servlet API, so you can use the
|
||||
following code snippet to get access to this "business context" `ApplicationContext`
|
||||
created by the `ContextLoaderListener`.
|
||||
|
||||
The following example shows how to get the `WebApplicationContext`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
|
||||
----
|
||||
|
||||
The
|
||||
{api-spring-framework}/web/context/support/WebApplicationContextUtils.html[`WebApplicationContextUtils`]
|
||||
class is for convenience, so you need not remember the name of the `ServletContext`
|
||||
attribute. Its `getWebApplicationContext()` method returns `null` if an object
|
||||
does not exist under the `WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`
|
||||
key. Rather than risk getting `NullPointerExceptions` in your application, it is better
|
||||
to use the `getRequiredWebApplicationContext()` method. This method throws an exception
|
||||
when the `ApplicationContext` is missing.
|
||||
|
||||
Once you have a reference to the `WebApplicationContext`, you can retrieve beans by their
|
||||
name or type. Most developers retrieve beans by name and then cast them to one of their
|
||||
implemented interfaces.
|
||||
|
||||
Fortunately, most of the frameworks in this section have simpler ways of looking up beans.
|
||||
Not only do they make it easy to get beans from a Spring container, but they also let you
|
||||
use dependency injection on their controllers. Each web framework section has more detail
|
||||
on its specific integration strategies.
|
||||
|
||||
|
||||
|
||||
|
||||
[[jsf]]
|
||||
== JSF
|
||||
|
||||
JavaServer Faces (JSF) is the JCP's standard component-based, event-driven web
|
||||
user interface framework. It is an official part of the Jakarta EE umbrella but also
|
||||
individually usable, e.g. through embedding Mojarra or MyFaces within Tomcat.
|
||||
|
||||
Please note that recent versions of JSF became closely tied to CDI infrastructure
|
||||
in application servers, with some new JSF functionality only working in such an
|
||||
environment. Spring's JSF support is not actively evolved anymore and primarily
|
||||
exists for migration purposes when modernizing older JSF-based applications.
|
||||
|
||||
The key element in Spring's JSF integration is the JSF `ELResolver` mechanism.
|
||||
|
||||
|
||||
|
||||
[[jsf-springbeanfaceselresolver]]
|
||||
=== Spring Bean Resolver
|
||||
|
||||
`SpringBeanFacesELResolver` is a JSF compliant `ELResolver` implementation,
|
||||
integrating with the standard Unified EL as used by JSF and JSP. It delegates to
|
||||
Spring's "business context" `WebApplicationContext` first and then to the
|
||||
default resolver of the underlying JSF implementation.
|
||||
|
||||
Configuration-wise, you can define `SpringBeanFacesELResolver` in your JSF
|
||||
`faces-context.xml` file, as the following example shows:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<faces-config>
|
||||
<application>
|
||||
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
|
||||
...
|
||||
</application>
|
||||
</faces-config>
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[jsf-facescontextutils]]
|
||||
=== Using `FacesContextUtils`
|
||||
|
||||
A custom `ELResolver` works well when mapping your properties to beans in
|
||||
`faces-config.xml`, but, at times, you may need to explicitly grab a bean.
|
||||
The {api-spring-framework}/web/jsf/FacesContextUtils.html[`FacesContextUtils`]
|
||||
class makes this easy. It is similar to `WebApplicationContextUtils`, except that
|
||||
it takes a `FacesContext` parameter rather than a `ServletContext` parameter.
|
||||
|
||||
The following example shows how to use `FacesContextUtils`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[struts]]
|
||||
== Apache Struts
|
||||
|
||||
Invented by Craig McClanahan, https://struts.apache.org[Struts] is an open-source project
|
||||
hosted by the Apache Software Foundation. Struts 1.x greatly simplified the
|
||||
JSP/Servlet programming paradigm and won over many developers who were using proprietary
|
||||
frameworks. It simplified the programming model; it was open source; and it had a large
|
||||
community, which let the project grow and become popular among Java web developers.
|
||||
|
||||
As a successor to the original Struts 1.x, check out Struts 2.x or more recent versions
|
||||
as well as the Struts-provided
|
||||
https://struts.apache.org/plugins/spring/[Spring Plugin] for built-in Spring integration.
|
||||
|
||||
|
||||
|
||||
|
||||
[[tapestry]]
|
||||
== Apache Tapestry
|
||||
|
||||
https://tapestry.apache.org/[Tapestry] is a "Component oriented framework for creating
|
||||
dynamic, robust, highly scalable web applications in Java."
|
||||
|
||||
While Spring has its own <<mvc, powerful web layer>>, there are a number of unique
|
||||
advantages to building an enterprise Java application by using a combination of Tapestry
|
||||
for the web user interface and the Spring container for the lower layers.
|
||||
|
||||
For more information, see Tapestry's dedicated
|
||||
https://tapestry.apache.org/integrating-with-spring-framework.html[integration module for Spring].
|
||||
|
||||
|
||||
|
||||
|
||||
[[web-integration-resources]]
|
||||
== Further Resources
|
||||
|
||||
The following links go to further resources about the various web frameworks described in
|
||||
this chapter.
|
||||
|
||||
* The https://www.oracle.com/java/technologies/javaserverfaces.html[JSF] homepage
|
||||
* The https://struts.apache.org/[Struts] homepage
|
||||
* The https://tapestry.apache.org/[Tapestry] homepage
|
||||
@@ -0,0 +1,95 @@
|
||||
In the context of web applications, _data binding_ involves the binding of HTTP request
|
||||
parameters (that is, form data or query parameters) to properties in a model object and
|
||||
its nested objects.
|
||||
|
||||
Only `public` properties following the
|
||||
https://www.oracle.com/java/technologies/javase/javabeans-spec.html[JavaBeans naming conventions]
|
||||
are exposed for data binding — for example, `public String getFirstName()` and
|
||||
`public void setFirstName(String)` methods for a `firstName` property.
|
||||
|
||||
TIP: The model object, and its nested object graph, is also sometimes referred to as a
|
||||
_command object_, _form-backing object_, or _POJO_ (Plain Old Java Object).
|
||||
|
||||
By default, Spring permits binding to all public properties in the model object graph.
|
||||
This means you need to carefully consider what public properties the model has, since a
|
||||
client could target any public property path, even some that are not expected to be
|
||||
targeted for a given use case.
|
||||
|
||||
For example, given an HTTP form data endpoint, a malicious client could supply values for
|
||||
properties that exist in the model object graph but are not part of the HTML form
|
||||
presented in the browser. This could lead to data being set on the model object and any
|
||||
of its nested objects, that is not expected to be updated.
|
||||
|
||||
The recommended approach is to use a _dedicated model object_ that exposes only
|
||||
properties that are relevant for the form submission. For example, on a form for changing
|
||||
a user's email address, the model object should declare a minimum set of properties such
|
||||
as in the following `ChangeEmailForm`.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public class ChangeEmailForm {
|
||||
|
||||
private String oldEmailAddress;
|
||||
private String newEmailAddress;
|
||||
|
||||
public void setOldEmailAddress(String oldEmailAddress) {
|
||||
this.oldEmailAddress = oldEmailAddress;
|
||||
}
|
||||
|
||||
public String getOldEmailAddress() {
|
||||
return this.oldEmailAddress;
|
||||
}
|
||||
|
||||
public void setNewEmailAddress(String newEmailAddress) {
|
||||
this.newEmailAddress = newEmailAddress;
|
||||
}
|
||||
|
||||
public String getNewEmailAddress() {
|
||||
return this.newEmailAddress;
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
If you cannot or do not want to use a _dedicated model object_ for each data
|
||||
binding use case, you **must** limit the properties that are allowed for data binding.
|
||||
Ideally, you can achieve this by registering _allowed field patterns_ via the
|
||||
`setAllowedFields()` method on `WebDataBinder`.
|
||||
|
||||
For example, to register allowed field patterns in your application, you can implement an
|
||||
`@InitBinder` method in a `@Controller` or `@ControllerAdvice` component as shown below:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Controller
|
||||
public class ChangeEmailController {
|
||||
|
||||
@InitBinder
|
||||
void initBinder(WebDataBinder binder) {
|
||||
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
|
||||
}
|
||||
|
||||
// @RequestMapping methods, etc.
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
In addition to registering allowed patterns, it is also possible to register _disallowed
|
||||
field patterns_ via the `setDisallowedFields()` method in `DataBinder` and its subclasses.
|
||||
Please note, however, that an "allow list" is safer than a "deny list". Consequently,
|
||||
`setAllowedFields()` should be favored over `setDisallowedFields()`.
|
||||
|
||||
Note that matching against allowed field patterns is case-sensitive; whereas, matching
|
||||
against disallowed field patterns is case-insensitive. In addition, a field matching a
|
||||
disallowed pattern will not be accepted even if it also happens to match a pattern in the
|
||||
allowed list.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
It is extremely important to properly configure allowed and disallowed field patterns
|
||||
when exposing your domain model directly for data binding purposes. Otherwise, it is a
|
||||
big security risk.
|
||||
|
||||
Furthermore, it is strongly recommended that you do **not** use types from your domain
|
||||
model such as JPA or Hibernate entities as the model object in data binding scenarios.
|
||||
====
|
||||
330
framework-docs/modules/ROOT/pages/web/web-uris.adoc
Normal file
330
framework-docs/modules/ROOT/pages/web/web-uris.adoc
Normal file
@@ -0,0 +1,330 @@
|
||||
[id={chapter}.web-uricomponents]
|
||||
= UriComponents
|
||||
[.small]#Spring MVC and Spring WebFlux#
|
||||
|
||||
`UriComponentsBuilder` helps to build URI's from URI templates with variables, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
UriComponents uriComponents = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}") // <1>
|
||||
.queryParam("q", "{q}") // <2>
|
||||
.encode() // <3>
|
||||
.build(); // <4>
|
||||
|
||||
URI uri = uriComponents.expand("Westin", "123").toUri(); // <5>
|
||||
----
|
||||
<1> Static factory method with a URI template.
|
||||
<2> Add or replace URI components.
|
||||
<3> Request to have the URI template and URI variables encoded.
|
||||
<4> Build a `UriComponents`.
|
||||
<5> Expand variables and obtain the `URI`.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uriComponents = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}") // <1>
|
||||
.queryParam("q", "{q}") // <2>
|
||||
.encode() // <3>
|
||||
.build() // <4>
|
||||
|
||||
val uri = uriComponents.expand("Westin", "123").toUri() // <5>
|
||||
----
|
||||
<1> Static factory method with a URI template.
|
||||
<2> Add or replace URI components.
|
||||
<3> Request to have the URI template and URI variables encoded.
|
||||
<4> Build a `UriComponents`.
|
||||
<5> Expand variables and obtain the `URI`.
|
||||
|
||||
The preceding example can be consolidated into one chain and shortened with `buildAndExpand`,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.encode()
|
||||
.buildAndExpand("Westin", "123")
|
||||
.toUri();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.encode()
|
||||
.buildAndExpand("Westin", "123")
|
||||
.toUri()
|
||||
----
|
||||
|
||||
You can shorten it further by going directly to a URI (which implies encoding),
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("Westin", "123");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("Westin", "123")
|
||||
----
|
||||
|
||||
You can shorten it further still with a full URI template, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
|
||||
.build("Westin", "123");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
|
||||
.build("Westin", "123")
|
||||
----
|
||||
|
||||
|
||||
|
||||
[id={chapter}.web-uribuilder]
|
||||
= UriBuilder
|
||||
[.small]#Spring MVC and Spring WebFlux#
|
||||
|
||||
<<{chapter}.web-uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a
|
||||
`UriBuilder`, in turn, with a `UriBuilderFactory`. Together, `UriBuilderFactory` and
|
||||
`UriBuilder` provide a pluggable mechanism to build URIs from URI templates, based on
|
||||
shared configuration, such as a base URL, encoding preferences, and other details.
|
||||
|
||||
You can configure `RestTemplate` and `WebClient` with a `UriBuilderFactory`
|
||||
to customize the preparation of URIs. `DefaultUriBuilderFactory` is a default
|
||||
implementation of `UriBuilderFactory` that uses `UriComponentsBuilder` internally and
|
||||
exposes shared configuration options.
|
||||
|
||||
The following example shows how to configure a `RestTemplate`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
|
||||
|
||||
String baseUrl = "https://example.org";
|
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
|
||||
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(factory);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
|
||||
|
||||
val baseUrl = "https://example.org"
|
||||
val factory = DefaultUriBuilderFactory(baseUrl)
|
||||
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
|
||||
|
||||
val restTemplate = RestTemplate()
|
||||
restTemplate.uriTemplateHandler = factory
|
||||
----
|
||||
|
||||
The following example configures a `WebClient`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
|
||||
|
||||
String baseUrl = "https://example.org";
|
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
|
||||
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
|
||||
|
||||
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
|
||||
|
||||
val baseUrl = "https://example.org"
|
||||
val factory = DefaultUriBuilderFactory(baseUrl)
|
||||
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
|
||||
|
||||
val client = WebClient.builder().uriBuilderFactory(factory).build()
|
||||
----
|
||||
|
||||
In addition, you can also use `DefaultUriBuilderFactory` directly. It is similar to using
|
||||
`UriComponentsBuilder` but, instead of static factory methods, it is an actual instance
|
||||
that holds configuration and preferences, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
String baseUrl = "https://example.com";
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
|
||||
|
||||
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("Westin", "123");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val baseUrl = "https://example.com"
|
||||
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)
|
||||
|
||||
val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("Westin", "123")
|
||||
----
|
||||
|
||||
|
||||
[id={chapter}.web-uri-encoding]
|
||||
= URI Encoding
|
||||
[.small]#Spring MVC and Spring WebFlux#
|
||||
|
||||
`UriComponentsBuilder` exposes encoding options at two levels:
|
||||
|
||||
* {api-spring-framework}/web/util/UriComponentsBuilder.html#encode--[UriComponentsBuilder#encode()]:
|
||||
Pre-encodes the URI template first and then strictly encodes URI variables when expanded.
|
||||
* {api-spring-framework}/web/util/UriComponents.html#encode--[UriComponents#encode()]:
|
||||
Encodes URI components _after_ URI variables are expanded.
|
||||
|
||||
Both options replace non-ASCII and illegal characters with escaped octets. However, the first option
|
||||
also replaces characters with reserved meaning that appear in URI variables.
|
||||
|
||||
TIP: Consider ";", which is legal in a path but has reserved meaning. The first option replaces
|
||||
";" with "%3B" in URI variables but not in the URI template. By contrast, the second option never
|
||||
replaces ";", since it is a legal character in a path.
|
||||
|
||||
For most cases, the first option is likely to give the expected result, because it treats URI
|
||||
variables as opaque data to be fully encoded, while the second option is useful if URI
|
||||
variables do intentionally contain reserved characters. The second option is also useful
|
||||
when not expanding URI variables at all since that will also encode anything that
|
||||
incidentally looks like a URI variable.
|
||||
|
||||
The following example uses the first option:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
|
||||
.queryParam("q", "{q}")
|
||||
.encode()
|
||||
.buildAndExpand("New York", "foo+bar")
|
||||
.toUri();
|
||||
|
||||
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
|
||||
.queryParam("q", "{q}")
|
||||
.encode()
|
||||
.buildAndExpand("New York", "foo+bar")
|
||||
.toUri()
|
||||
|
||||
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
|
||||
----
|
||||
|
||||
You can shorten the preceding example by going directly to the URI (which implies encoding),
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("New York", "foo+bar");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("New York", "foo+bar")
|
||||
----
|
||||
|
||||
You can shorten it further still with a full URI template, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
|
||||
.build("New York", "foo+bar");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
|
||||
.build("New York", "foo+bar")
|
||||
----
|
||||
|
||||
The `WebClient` and the `RestTemplate` expand and encode URI templates internally through
|
||||
the `UriBuilderFactory` strategy. Both can be configured with a custom strategy,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
String baseUrl = "https://example.com";
|
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
|
||||
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
|
||||
|
||||
// Customize the RestTemplate..
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(factory);
|
||||
|
||||
// Customize the WebClient..
|
||||
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val baseUrl = "https://example.com"
|
||||
val factory = DefaultUriBuilderFactory(baseUrl).apply {
|
||||
encodingMode = EncodingMode.TEMPLATE_AND_VALUES
|
||||
}
|
||||
|
||||
// Customize the RestTemplate..
|
||||
val restTemplate = RestTemplate().apply {
|
||||
uriTemplateHandler = factory
|
||||
}
|
||||
|
||||
// Customize the WebClient..
|
||||
val client = WebClient.builder().uriBuilderFactory(factory).build()
|
||||
----
|
||||
|
||||
The `DefaultUriBuilderFactory` implementation uses `UriComponentsBuilder` internally to
|
||||
expand and encode URI templates. As a factory, it provides a single place to configure
|
||||
the approach to encoding, based on one of the below encoding modes:
|
||||
|
||||
* `TEMPLATE_AND_VALUES`: Uses `UriComponentsBuilder#encode()`, corresponding to
|
||||
the first option in the earlier list, to pre-encode the URI template and strictly encode URI variables when
|
||||
expanded.
|
||||
* `VALUES_ONLY`: Does not encode the URI template and, instead, applies strict encoding
|
||||
to URI variables through `UriUtils#encodeUriVariables` prior to expanding them into the
|
||||
template.
|
||||
* `URI_COMPONENT`: Uses `UriComponents#encode()`, corresponding to the second option in the earlier list, to
|
||||
encode URI component value _after_ URI variables are expanded.
|
||||
* `NONE`: No encoding is applied.
|
||||
|
||||
The `RestTemplate` is set to `EncodingMode.URI_COMPONENT` for historic
|
||||
reasons and for backwards compatibility. The `WebClient` relies on the default value
|
||||
in `DefaultUriBuilderFactory`, which was changed from `EncodingMode.URI_COMPONENT` in
|
||||
5.0.x to `EncodingMode.TEMPLATE_AND_VALUES` in 5.1.
|
||||
367
framework-docs/modules/ROOT/pages/web/webflux-cors.adoc
Normal file
367
framework-docs/modules/ROOT/pages/web/webflux-cors.adoc
Normal file
@@ -0,0 +1,367 @@
|
||||
[[webflux-cors]]
|
||||
= CORS
|
||||
[.small]#<<web.adoc#mvc-cors, See equivalent in the Servlet stack>>#
|
||||
|
||||
Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
describes how to do so.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-intro]]
|
||||
== Introduction
|
||||
[.small]#<<web.adoc#mvc-cors-intro, See equivalent in the Servlet stack>>#
|
||||
|
||||
For security reasons, browsers prohibit AJAX calls to resources outside the current origin.
|
||||
For example, you could have your bank account in one tab and evil.com in another. Scripts
|
||||
from evil.com should not be able to make AJAX requests to your bank API with your
|
||||
credentials -- for example, withdrawing money from your account!
|
||||
|
||||
Cross-Origin Resource Sharing (CORS) is a https://www.w3.org/TR/cors/[W3C specification]
|
||||
implemented by https://caniuse.com/#feat=cors[most browsers] that lets you specify
|
||||
what kind of cross-domain requests are authorized, rather than using less secure and less
|
||||
powerful workarounds based on IFRAME or JSONP.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-processing]]
|
||||
== Processing
|
||||
[.small]#<<web.adoc#mvc-cors-processing, See equivalent in the Servlet stack>>#
|
||||
|
||||
The CORS specification distinguishes between preflight, simple, and actual requests.
|
||||
To learn how CORS works, you can read
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[this article], among
|
||||
many others, or see the specification for more details.
|
||||
|
||||
Spring WebFlux `HandlerMapping` implementations provide built-in support for CORS. After successfully
|
||||
mapping a request to a handler, a `HandlerMapping` checks the CORS configuration for the
|
||||
given request and handler and takes further actions. Preflight requests are handled
|
||||
directly, while simple and actual CORS requests are intercepted, validated, and have the
|
||||
required CORS response headers set.
|
||||
|
||||
In order to enable cross-origin requests (that is, the `Origin` header is present and
|
||||
differs from the host of the request), you need to have some explicitly declared CORS
|
||||
configuration. If no matching CORS configuration is found, preflight requests are
|
||||
rejected. No CORS headers are added to the responses of simple and actual CORS requests
|
||||
and, consequently, browsers reject them.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{api-spring-framework}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
|
||||
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
|
||||
use the WebFlux Java configuration to declare such mappings, which results in a single,
|
||||
global map passed to all `HandlerMapping` implementations.
|
||||
|
||||
You can combine global CORS configuration at the `HandlerMapping` level with more
|
||||
fine-grained, handler-level CORS configuration. For example, annotated controllers can use
|
||||
class- or method-level `@CrossOrigin` annotations (other handlers can implement
|
||||
`CorsConfigurationSource`).
|
||||
|
||||
The rules for combining global and local configuration are generally additive -- for example,
|
||||
all global and all local origins. For those attributes where only a single value can be
|
||||
accepted, such as `allowCredentials` and `maxAge`, the local overrides the global value. See
|
||||
{api-spring-framework}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
for more details.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
To learn more from the source or to make advanced customizations, see:
|
||||
|
||||
* `CorsConfiguration`
|
||||
* `CorsProcessor` and `DefaultCorsProcessor`
|
||||
* `AbstractHandlerMapping`
|
||||
====
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-controller]]
|
||||
== `@CrossOrigin`
|
||||
[.small]#<<web.adoc#mvc-cors-controller, See equivalent in the Servlet stack>>#
|
||||
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Account> retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<Void> 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:
|
||||
|
||||
* All origins.
|
||||
* All headers.
|
||||
* All HTTP methods to which the controller method is mapped.
|
||||
|
||||
`allowCredentials` is not enabled by default, since that establishes a trust level
|
||||
that exposes sensitive user-specific information (such as cookies and CSRF tokens) and
|
||||
should be used only where appropriate. When it is enabled either `allowOrigins` must be
|
||||
set to one or more specific domain (but not the special value `"*"`) or alternatively
|
||||
the `allowOriginPatterns` property may be used to match to a dynamic set of origins.
|
||||
|
||||
`maxAge` is set to 30 minutes.
|
||||
|
||||
`@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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Account> retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<Void> 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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@CrossOrigin(maxAge = 3600) // <1>
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin("https://domain2.com") // <2>
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Account> retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<Void> 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.
|
||||
--
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-global]]
|
||||
== Global Configuration
|
||||
[.small]#<<web.adoc#mvc-cors-global, See equivalent in the Servlet stack>>#
|
||||
|
||||
In addition to fine-grained, controller method-level configuration, you probably want to
|
||||
define some global CORS configuration, too. You can set URL-based `CorsConfiguration`
|
||||
mappings individually on any `HandlerMapping`. Most applications, however, use the
|
||||
WebFlux Java configuration to do that.
|
||||
|
||||
By default global configuration enables the following:
|
||||
|
||||
* All origins.
|
||||
* All headers.
|
||||
* `GET`, `HEAD`, and `POST` methods.
|
||||
|
||||
`allowedCredentials` is not enabled by default, since that establishes a trust level
|
||||
that exposes sensitive user-specific information (such as cookies and CSRF tokens) and
|
||||
should be used only where appropriate. When it is enabled either `allowOrigins` must be
|
||||
set to one or more specific domain (but not the special value `"*"`) or alternatively
|
||||
the `allowOriginPatterns` property may be used to match to a dynamic set of origins.
|
||||
|
||||
`maxAge` is set to 30 minutes.
|
||||
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@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);
|
||||
|
||||
// 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...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-webfilter]]
|
||||
== CORS `WebFilter`
|
||||
[.small]#<<web.adoc#mvc-cors-filter, See equivalent in the Servlet stack>>#
|
||||
|
||||
You can apply CORS support through the built-in
|
||||
{api-spring-framework}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a
|
||||
good fit with <<webflux-fn, functional endpoints>>.
|
||||
|
||||
NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring
|
||||
Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] 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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Bean
|
||||
CorsWebFilter corsFilter() {
|
||||
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
|
||||
// Possibly...
|
||||
// config.applyPermitDefaultValues()
|
||||
|
||||
config.setAllowCredentials(true);
|
||||
config.addAllowedOrigin("https://domain1.com");
|
||||
config.addAllowedHeader("*");
|
||||
config.addAllowedMethod("*");
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
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)
|
||||
}
|
||||
----
|
||||
905
framework-docs/modules/ROOT/pages/web/webflux-functional.adoc
Normal file
905
framework-docs/modules/ROOT/pages/web/webflux-functional.adoc
Normal file
@@ -0,0 +1,905 @@
|
||||
[[webflux-fn]]
|
||||
= Functional Endpoints
|
||||
[.small]#<<web.adoc#webmvc-fn, See equivalent in the Servlet stack>>#
|
||||
|
||||
Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions
|
||||
are used to route and handle requests and contracts are designed for immutability.
|
||||
It is an alternative to the annotation-based programming model but otherwise runs on
|
||||
the same <<web-reactive.adoc#webflux-reactive-spring-web>> foundation.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-overview]]
|
||||
== Overview
|
||||
[.small]#<<web.adoc#webmvc-fn-overview, See equivalent in the Servlet stack>>#
|
||||
|
||||
In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes
|
||||
`ServerRequest` and returns a delayed `ServerResponse` (i.e. `Mono<ServerResponse>`).
|
||||
Both the request and the response object have immutable contracts that offer JDK 8-friendly
|
||||
access to the HTTP request and response.
|
||||
`HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the
|
||||
annotation-based programming model.
|
||||
|
||||
Incoming requests are routed to a handler function with a `RouterFunction`: a function that
|
||||
takes `ServerRequest` and returns a delayed `HandlerFunction` (i.e. `Mono<HandlerFunction>`).
|
||||
When the router function matches, a handler function is returned; otherwise an empty Mono.
|
||||
`RouterFunction` is the equivalent of a `@RequestMapping` annotation, but with the major
|
||||
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",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;
|
||||
|
||||
PersonRepository repository = ...
|
||||
PersonHandler handler = new PersonHandler(repository);
|
||||
|
||||
RouterFunction<ServerResponse> route = route() <1>
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST("/person", handler::createPerson)
|
||||
.build();
|
||||
|
||||
|
||||
public class PersonHandler {
|
||||
|
||||
// ...
|
||||
|
||||
public Mono<ServerResponse> listPeople(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> createPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using `route()`.
|
||||
|
||||
[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 <<web-reactive.adoc#webflux-httphandler, server adapters>>:
|
||||
|
||||
* `RouterFunctions.toHttpHandler(RouterFunction)`
|
||||
* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)`
|
||||
|
||||
Most applications can run through the WebFlux Java configuration, see <<webflux-fn-running>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-handler-functions]]
|
||||
== HandlerFunction
|
||||
[.small]#<<web.adoc#webmvc-fn-handler-functions, See equivalent in the Servlet stack>>#
|
||||
|
||||
`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly
|
||||
access to the HTTP request and response.
|
||||
Both request and response provide https://www.reactive-streams.org[Reactive Streams] back pressure
|
||||
against the body streams.
|
||||
The request body is represented with a Reactor `Flux` or `Mono`.
|
||||
The response body is represented with any Reactive Streams `Publisher`, including `Flux` and `Mono`.
|
||||
For more on that, see <<web-reactive.adoc#webflux-reactive-libraries, Reactive Libraries>>.
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-request]]
|
||||
=== ServerRequest
|
||||
|
||||
`ServerRequest` provides access to the HTTP method, URI, headers, and query parameters,
|
||||
while access to the body is provided through the `body` methods.
|
||||
|
||||
The following example extracts the request body to a `Mono<String>`:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<String> string = request.bodyToMono(String.class);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val string = request.awaitBody<String>()
|
||||
----
|
||||
|
||||
|
||||
The following example extracts the body to a `Flux<Person>` (or a `Flow<Person>` in Kotlin),
|
||||
where `Person` objects are decoded from some serialized form, such as JSON or XML:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Flux<Person> people = request.bodyToFlux(Person.class);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val people = request.bodyToFlow<Person>()
|
||||
----
|
||||
|
||||
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,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
|
||||
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
|
||||
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
|
||||
----
|
||||
|
||||
The following example shows how to access form data:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<MultiValueMap<String, String>> 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,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<MultiValueMap<String, Part>> map = request.multipartData();
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val map = request.awaitMultipartData()
|
||||
----
|
||||
|
||||
The following example shows how to access multipart data, one at a time, in streaming fashion:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
Flux<PartEvent> allPartEvents = request.bodyToFlux(PartEvent.class);
|
||||
allPartsEvents.windowUntil(PartEvent::isLast)
|
||||
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
|
||||
if (signal.hasValue()) {
|
||||
PartEvent event = signal.get();
|
||||
if (event instanceof FormPartEvent formEvent) {
|
||||
String value = formEvent.value();
|
||||
// handle form field
|
||||
}
|
||||
else if (event instanceof FilePartEvent fileEvent) {
|
||||
String filename = fileEvent.filename();
|
||||
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
|
||||
// handle file upload
|
||||
}
|
||||
else {
|
||||
return Mono.error(new RuntimeException("Unexpected event: " + event));
|
||||
}
|
||||
}
|
||||
else {
|
||||
return partEvents; // either complete or error signal
|
||||
}
|
||||
}));
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val parts = request.bodyToFlux<PartEvent>()
|
||||
allPartsEvents.windowUntil(PartEvent::isLast)
|
||||
.concatMap {
|
||||
it.switchOnFirst { signal, partEvents ->
|
||||
if (signal.hasValue()) {
|
||||
val event = signal.get()
|
||||
if (event is FormPartEvent) {
|
||||
val value: String = event.value();
|
||||
// handle form field
|
||||
} else if (event is FilePartEvent) {
|
||||
val filename: String = event.filename();
|
||||
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content);
|
||||
// handle file upload
|
||||
} else {
|
||||
return Mono.error(RuntimeException("Unexpected event: " + event));
|
||||
}
|
||||
} else {
|
||||
return partEvents; // either complete or error signal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Note that the body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
|
||||
|
||||
[[webflux-fn-response]]
|
||||
=== ServerResponse
|
||||
|
||||
`ServerResponse` provides access to the HTTP response and, since it is immutable, you can use
|
||||
a `build` method to create it. You can use the builder to set the response status, to add response
|
||||
headers, or to provide a body. The following example creates a 200 (OK) response with JSON
|
||||
content:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<Person> person = ...
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val person: Person = ...
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
|
||||
----
|
||||
|
||||
The following example shows how to build a 201 (CREATED) response with a `Location` header and no body:
|
||||
|
||||
[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,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).body(...)
|
||||
----
|
||||
|
||||
|
||||
[[webflux-fn-handler-classes]]
|
||||
=== Handler Classes
|
||||
|
||||
We can write a handler function as a lambda, as the following example shows:
|
||||
|
||||
--
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
HandlerFunction<ServerResponse> helloWorld =
|
||||
request -> ServerResponse.ok().bodyValue("Hello World");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
|
||||
----
|
||||
--
|
||||
|
||||
That is convenient, but in an application we need multiple functions, and multiple inline
|
||||
lambda's can get messy.
|
||||
Therefore, it is useful to group related handler functions together into a handler class, which
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
|
||||
|
||||
public class PersonHandler {
|
||||
|
||||
private final PersonRepository repository;
|
||||
|
||||
public PersonHandler(PersonRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> listPeople(ServerRequest request) { // <1>
|
||||
Flux<Person> people = repository.allPeople();
|
||||
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> createPerson(ServerRequest request) { // <2>
|
||||
Mono<Person> person = request.bodyToMono(Person.class);
|
||||
return ok().build(repository.savePerson(person));
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getPerson(ServerRequest request) { // <3>
|
||||
int personId = Integer.valueOf(request.pathVariable("id"));
|
||||
return repository.getPerson(personId)
|
||||
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
}
|
||||
----
|
||||
<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)` returns `Mono<Void>`: an empty `Mono` that emits
|
||||
a completion signal when the person has been read from the request and stored. So we use the
|
||||
`build(Publisher<Void>)` method to send a response when that completion signal is received (that is,
|
||||
when the `Person` has been saved).
|
||||
<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 use `switchIfEmpty(Mono<T>)` 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<Person> = repository.allPeople()
|
||||
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
|
||||
}
|
||||
|
||||
suspend fun createPerson(request: ServerRequest): ServerResponse { // <2>
|
||||
val person = request.awaitBody<Person>()
|
||||
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).bodyValueAndAwait(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]]
|
||||
=== Validation
|
||||
|
||||
A functional endpoint can use Spring's <<core.adoc#validation, validation facilities>> to
|
||||
apply validation to the request body. For example, given a custom Spring
|
||||
<<core.adoc#validation, Validator>> implementation for a `Person`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
public class PersonHandler {
|
||||
|
||||
private final Validator validator = new PersonValidator(); // <1>
|
||||
|
||||
// ...
|
||||
|
||||
public Mono<ServerResponse> createPerson(ServerRequest request) {
|
||||
Mono<Person> 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>
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
<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<Person>()
|
||||
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`.
|
||||
See <<core.adoc#validation-beanvalidation, Spring Validation>>.
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-router-functions]]
|
||||
== `RouterFunction`
|
||||
[.small]#<<web.adoc#webmvc-fn-router-functions, See equivalent in the Servlet stack>>#
|
||||
|
||||
Router functions are used to route the requests to the corresponding `HandlerFunction`.
|
||||
Typically, you do not write router functions yourself, but rather use a method on the
|
||||
`RouterFunctions` utility class to create one.
|
||||
`RouterFunctions.route()` (no parameters) provides you with a fluent builder for creating a router
|
||||
function, whereas `RouterFunctions.route(RequestPredicate, HandlerFunction)` offers a direct way
|
||||
to create a router.
|
||||
|
||||
Generally, it is recommended to use the `route()` builder, as it provides
|
||||
convenient short-cuts for typical mapping scenarios without requiring hard-to-discover
|
||||
static imports.
|
||||
For instance, the router function builder offers the method `GET(String, HandlerFunction)` to create a mapping for GET requests; and `POST(String, HandlerFunction)` for POSTs.
|
||||
|
||||
Besides HTTP method-based mapping, the route builder offers a way to introduce additional
|
||||
predicates when mapping to requests.
|
||||
For each HTTP method there is an overloaded variant that takes a `RequestPredicate` as a
|
||||
parameter, though which additional constraints can be expressed.
|
||||
|
||||
|
||||
[[webflux-fn-predicates]]
|
||||
=== Predicates
|
||||
|
||||
You can write your own `RequestPredicate`, but the `RequestPredicates` utility class
|
||||
offers commonly used implementations, based on the request path, HTTP method, content-type,
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = RouterFunctions.route()
|
||||
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
|
||||
request -> ServerResponse.ok().bodyValue("Hello World")).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val route = coRouter {
|
||||
GET("/hello-world", accept(TEXT_PLAIN)) {
|
||||
ServerResponse.ok().bodyValueAndAwait("Hello World")
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
You can compose multiple request predicates together by using:
|
||||
|
||||
* `RequestPredicate.and(RequestPredicate)` -- both must match.
|
||||
* `RequestPredicate.or(RequestPredicate)` -- either can match.
|
||||
|
||||
Many of the predicates from `RequestPredicates` are composed.
|
||||
For example, `RequestPredicates.GET(String)` is composed from `RequestPredicates.method(HttpMethod)`
|
||||
and `RequestPredicates.path(String)`.
|
||||
The example shown above also uses two request predicates, as the builder uses
|
||||
`RequestPredicates.GET` internally, and composes that with the `accept` predicate.
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-routes]]
|
||||
=== Routes
|
||||
|
||||
Router functions are evaluated in order: if the first route does not match, the
|
||||
second is evaluated, and so on.
|
||||
Therefore, it makes sense to declare more specific routes before general ones.
|
||||
This is also important when registering router functions as Spring beans, as will
|
||||
be described later.
|
||||
Note that this behavior is different from the annotation-based programming model, where the
|
||||
"most specific" controller method is picked automatically.
|
||||
|
||||
When using the router function builder, all defined routes are composed into one
|
||||
`RouterFunction` that is returned from `build()`.
|
||||
There are also other ways to compose multiple router functions together:
|
||||
|
||||
* `add(RouterFunction)` on the `RouterFunctions.route()` builder
|
||||
* `RouterFunction.and(RouterFunction)`
|
||||
* `RouterFunction.andRoute(RequestPredicate, HandlerFunction)` -- shortcut for
|
||||
`RouterFunction.and()` with nested `RouterFunctions.route()`.
|
||||
|
||||
The following example shows the composition of four routes:
|
||||
|
||||
|
||||
[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.*;
|
||||
|
||||
PersonRepository repository = ...
|
||||
PersonHandler handler = new PersonHandler(repository);
|
||||
|
||||
RouterFunction<ServerResponse> otherRoute = ...
|
||||
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
|
||||
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
|
||||
.POST("/person", handler::createPerson) // <3>
|
||||
.add(otherRoute) // <4>
|
||||
.build();
|
||||
----
|
||||
<1> pass:q[`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.
|
||||
|
||||
[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<ServerResponse> = coRouter { }
|
||||
|
||||
val route = coRouter {
|
||||
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
|
||||
GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
|
||||
POST("/person", handler::createPerson) // <3>
|
||||
}.and(otherRoute) // <4>
|
||||
----
|
||||
<1> pass:q[`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
|
||||
|
||||
It is common for a group of router functions to have a shared predicate, for instance a
|
||||
shared path. In the example above, the shared predicate would be a path predicate that
|
||||
matches `/person`, used by three of the routes. When using annotations, you would remove
|
||||
this duplication by using a type-level `@RequestMapping` annotation that maps to
|
||||
`/person`. 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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", builder -> builder // <1>
|
||||
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST(handler::createPerson))
|
||||
.build();
|
||||
----
|
||||
<1> Note that second parameter of `path` is a consumer that takes the router builder.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val route = coRouter { // <1>
|
||||
"/person".nest {
|
||||
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using Coroutines router DSL; a Reactive alternative is also available via `router { }`.
|
||||
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(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(handler::createPerson)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[[webflux-fn-running]]
|
||||
== Running a Server
|
||||
[.small]#<<web.adoc#webmvc-fn-running, See equivalent in the Servlet stack>>#
|
||||
|
||||
How do you run a router function in an HTTP server? A simple option is to convert a router
|
||||
function to an `HttpHandler` by using one of the following:
|
||||
|
||||
* `RouterFunctions.toHttpHandler(RouterFunction)`
|
||||
* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)`
|
||||
|
||||
You can then use the returned `HttpHandler` with a number of server adapters by following
|
||||
<<web-reactive.adoc#webflux-httphandler, HttpHandler>> for server-specific instructions.
|
||||
|
||||
A more typical option, also used by Spring Boot, is to run with a
|
||||
<<web-reactive.adoc#webflux-dispatcher-handler, `DispatcherHandler`>>-based setup through the
|
||||
<<web-reactive.adoc#webflux-config>>, which uses Spring configuration to declare the
|
||||
components required to process requests. The WebFlux Java configuration declares the following
|
||||
infrastructure components to support functional endpoints:
|
||||
|
||||
* `RouterFunctionMapping`: Detects one or more `RouterFunction<?>` beans in the Spring
|
||||
configuration, <<core.adoc#beans-factory-ordered, orders them>>, combines them through
|
||||
`RouterFunction.andOther`, and routes requests to the resulting composed `RouterFunction`.
|
||||
* `HandlerFunctionAdapter`: Simple adapter that lets `DispatcherHandler` invoke
|
||||
a `HandlerFunction` that was mapped to a request.
|
||||
* `ServerResponseResultHandler`: Handles the result from the invocation of a
|
||||
`HandlerFunction` by invoking the `writeTo` method of the `ServerResponse`.
|
||||
|
||||
The preceding components let functional endpoints fit within the `DispatcherHandler` request
|
||||
processing lifecycle and also (potentially) run side by side with annotated controllers, if
|
||||
any are declared. It is also how functional endpoints are enabled by the Spring Boot WebFlux
|
||||
starter.
|
||||
|
||||
The following example shows a WebFlux Java configuration (see
|
||||
<<web-reactive.adoc#webflux-dispatcher-handler, DispatcherHandler>> for how to run it):
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@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 fun addCorsMappings(registry: CorsRegistry) {
|
||||
// configure CORS...
|
||||
}
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
// configure view resolution for HTML rendering...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-handler-filter-function]]
|
||||
== Filtering Handler Functions
|
||||
[.small]#<<web.adoc#webmvc-fn-handler-filter-function, See equivalent in the Servlet stack>>#
|
||||
|
||||
You can filter handler functions by using the `before`, `after`, or `filter` methods on the routing
|
||||
function builder.
|
||||
With annotations, you can achieve similar functionality by using `@ControllerAdvice`, a `ServletFilter`, or both.
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople)
|
||||
.before(request -> ServerRequest.from(request) // <1>
|
||||
.header("X-RequestHeader", "Value")
|
||||
.build()))
|
||||
.POST(handler::createPerson))
|
||||
.after((request, response) -> logResponse(response)) // <2>
|
||||
.build();
|
||||
----
|
||||
<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
|
||||
----
|
||||
val route = router {
|
||||
"/person".nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
before { // <1>
|
||||
ServerRequest.from(it)
|
||||
.header("X-RequestHeader", "Value").build()
|
||||
}
|
||||
POST(handler::createPerson)
|
||||
after { _, response -> // <2>
|
||||
logResponse(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
<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.
|
||||
|
||||
|
||||
The `filter` method on the router builder takes a `HandlerFilterFunction`: a
|
||||
function that takes a `ServerRequest` and `HandlerFunction` and returns a `ServerResponse`.
|
||||
The handler function parameter represents the next element in the chain.
|
||||
This is typically the handler that is routed to, but it can also be another
|
||||
filter if multiple are applied.
|
||||
|
||||
Now we can add a simple security filter to our route, assuming that we have a `SecurityManager` that
|
||||
can determine whether a particular path is allowed.
|
||||
The following example shows how to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
SecurityManager securityManager = ...
|
||||
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(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
|
||||
----
|
||||
val securityManager: SecurityManager = ...
|
||||
|
||||
val route = router {
|
||||
("/person" and accept(APPLICATION_JSON)).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
filter { request, next ->
|
||||
if (securityManager.allowAccessTo(request.path())) {
|
||||
next(request)
|
||||
}
|
||||
else {
|
||||
status(UNAUTHORIZED).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional.
|
||||
We only let the handler function be run when access is allowed.
|
||||
|
||||
Besides using the `filter` method on the router function builder, it is possible to apply a
|
||||
filter to an existing router function via `RouterFunction.filter(HandlerFilterFunction)`.
|
||||
|
||||
NOTE: CORS support for functional endpoints is provided through a dedicated
|
||||
<<webflux-cors.adoc#webflux-cors-webfilter, `CorsWebFilter`>>.
|
||||
408
framework-docs/modules/ROOT/pages/web/webflux-view.adoc
Normal file
408
framework-docs/modules/ROOT/pages/web/webflux-view.adoc
Normal file
@@ -0,0 +1,408 @@
|
||||
[[webflux-view]]
|
||||
= View Technologies
|
||||
[.small]#<<web.adoc#mvc-view, See equivalent in the Servlet stack>>#
|
||||
|
||||
The use of view technologies in Spring WebFlux is pluggable. Whether you decide to
|
||||
use Thymeleaf, FreeMarker, or some other view technology is primarily a matter of a
|
||||
configuration change. This chapter covers the view technologies integrated with Spring
|
||||
WebFlux. We assume you are already familiar with <<webflux-viewresolution>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-view-thymeleaf]]
|
||||
== Thymeleaf
|
||||
[.small]#<<web.adoc#mvc-view-thymeleaf, See equivalent in the Servlet stack>>#
|
||||
|
||||
Thymeleaf is a modern server-side Java template engine that emphasizes natural HTML
|
||||
templates that can be previewed in a browser by double-clicking, which is very
|
||||
helpful for independent work on UI templates (for example, by a designer) without the need for a
|
||||
running server. Thymeleaf offers an extensive set of features, and it is actively developed
|
||||
and maintained. For a more complete introduction, see the
|
||||
https://www.thymeleaf.org/[Thymeleaf] project home page.
|
||||
|
||||
The Thymeleaf integration with Spring WebFlux is managed by the Thymeleaf project. The
|
||||
configuration involves a few bean declarations, such as
|
||||
`SpringResourceTemplateResolver`, `SpringWebFluxTemplateEngine`, and
|
||||
`ThymeleafReactiveViewResolver`. For more details, see
|
||||
https://www.thymeleaf.org/documentation.html[Thymeleaf+Spring] and the WebFlux integration
|
||||
https://web.archive.org/web/20210623051330/http%3A//forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html[announcement].
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-view-freemarker]]
|
||||
== FreeMarker
|
||||
[.small]#<<web.adoc#mvc-view-freemarker, See equivalent in the Servlet stack>>#
|
||||
|
||||
https://freemarker.apache.org/[Apache FreeMarker] is a template engine for generating any
|
||||
kind of text output from HTML to email and others. The Spring Framework has built-in
|
||||
integration for using Spring WebFlux with FreeMarker templates.
|
||||
|
||||
|
||||
|
||||
[[webflux-view-freemarker-contextconfig]]
|
||||
=== View Configuration
|
||||
[.small]#<<web.adoc#mvc-view-freemarker-contextconfig, See equivalent in the Servlet stack>>#
|
||||
|
||||
The following example shows how to configure FreeMarker as a view technology:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.freeMarker();
|
||||
}
|
||||
|
||||
// Configure FreeMarker...
|
||||
|
||||
@Bean
|
||||
public FreeMarkerConfigurer freeMarkerConfigurer() {
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
----
|
||||
[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
|
||||
returns the view name, `welcome`, the resolver looks for the
|
||||
`classpath:/templates/freemarker/welcome.ftl` template.
|
||||
|
||||
|
||||
|
||||
[[webflux-views-freemarker]]
|
||||
=== FreeMarker Configuration
|
||||
[.small]#<<web.adoc#mvc-views-freemarker, See equivalent in the Servlet stack>>#
|
||||
|
||||
You can pass FreeMarker 'Settings' and 'SharedVariables' directly to the FreeMarker
|
||||
`Configuration` object (which is managed by Spring) by setting the appropriate bean
|
||||
properties on the `FreeMarkerConfigurer` bean. The `freemarkerSettings` property requires
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
// ...
|
||||
|
||||
@Bean
|
||||
public FreeMarkerConfigurer freeMarkerConfigurer() {
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("xml_escape", new XmlEscape());
|
||||
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setTemplateLoaderPath("classpath:/templates");
|
||||
configurer.setFreemarkerVariables(variables);
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
----
|
||||
[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.
|
||||
|
||||
|
||||
|
||||
[[webflux-view-freemarker-forms]]
|
||||
=== Form Handling
|
||||
[.small]#<<web.adoc#mvc-view-freemarker-forms, See equivalent in the Servlet stack>>#
|
||||
|
||||
Spring provides a tag library for use in JSPs that contains, among others, a
|
||||
`<spring:bind/>` element. This element primarily lets forms display values from
|
||||
form-backing objects and show the results of failed validations from a `Validator` in the
|
||||
web or business tier. Spring also has support for the same functionality in FreeMarker,
|
||||
with additional convenience macros for generating form input elements themselves.
|
||||
|
||||
|
||||
[[webflux-view-bind-macros]]
|
||||
==== The Bind Macros
|
||||
[.small]#<<web.adoc#mvc-view-bind-macros, See equivalent in the Servlet stack>>#
|
||||
|
||||
A standard set of macros are maintained within the `spring-webflux.jar` file for
|
||||
FreeMarker, so they are always available to a suitably configured application.
|
||||
|
||||
Some of the macros defined in the Spring templating libraries are considered internal
|
||||
(private), but no such scoping exists in the macro definitions, making all macros visible
|
||||
to calling code and user templates. The following sections concentrate only on the macros
|
||||
you need to directly call from within your templates. If you wish to view the macro code
|
||||
directly, the file is called `spring.ftl` and is in the
|
||||
`org.springframework.web.reactive.result.view.freemarker` package.
|
||||
|
||||
For additional details on binding support, see <<web.adoc#mvc-view-simple-binding, Simple
|
||||
Binding>> for Spring MVC.
|
||||
|
||||
|
||||
[[webflux-views-form-macros]]
|
||||
==== Form Macros
|
||||
|
||||
For details on Spring's form macro support for FreeMarker templates, consult the following
|
||||
sections of the Spring MVC documentation.
|
||||
|
||||
* <<web.adoc#mvc-views-form-macros, Input Macros>>
|
||||
* <<web.adoc#mvc-views-form-macros-input, Input Fields>>
|
||||
* <<web.adoc#mvc-views-form-macros-select, Selection Fields>>
|
||||
* <<web.adoc#mvc-views-form-macros-html-escaping, HTML Escaping>>
|
||||
|
||||
|
||||
|
||||
[[webflux-view-script]]
|
||||
== Script Views
|
||||
[.small]#<<web.adoc#mvc-view-script, See equivalent in the Servlet stack>>#
|
||||
|
||||
The Spring Framework has a built-in integration for using Spring WebFlux with any
|
||||
templating library that can run on top of the
|
||||
https://www.jcp.org/en/jsr/detail?id=223[JSR-223] Java scripting engine.
|
||||
The following table shows the templating libraries that we have tested on different script engines:
|
||||
|
||||
[%header]
|
||||
|===
|
||||
|Scripting Library |Scripting Engine
|
||||
|https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://facebook.github.io/react/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://www.embeddedjs.com/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://www.stuartellis.name/articles/erb/[ERB] |https://www.jruby.org[JRuby]
|
||||
|https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython]
|
||||
|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |https://kotlinlang.org/[Kotlin]
|
||||
|===
|
||||
|
||||
TIP: The basic rule for integrating any other script engine is that it must implement the
|
||||
`ScriptEngine` and `Invocable` interfaces.
|
||||
|
||||
|
||||
|
||||
[[webflux-view-script-dependencies]]
|
||||
=== Requirements
|
||||
[.small]#<<web.adoc#mvc-view-script-dependencies, See equivalent in the Servlet stack>>#
|
||||
|
||||
You need to have the script engine on your classpath, the details of which vary by script engine:
|
||||
|
||||
* The https://openjdk.java.net/projects/nashorn/[Nashorn] JavaScript engine is provided with
|
||||
Java 8+. Using the latest update release available is highly recommended.
|
||||
* https://www.jruby.org[JRuby] should be added as a dependency for Ruby support.
|
||||
* https://www.jython.org[Jython] should be added as a dependency for Python support.
|
||||
* `org.jetbrains.kotlin:kotlin-script-util` dependency and a `META-INF/services/javax.script.ScriptEngineFactory`
|
||||
file containing a `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`
|
||||
line should be added for Kotlin script support. See
|
||||
https://github.com/sdeleuze/kotlin-script-templating[this example] for more detail.
|
||||
|
||||
You need to have the script templating library. One way to do that for JavaScript is
|
||||
through https://www.webjars.org/[WebJars].
|
||||
|
||||
|
||||
|
||||
[[webflux-view-script-integrate]]
|
||||
=== Script Templates
|
||||
[.small]#<<web.adoc#mvc-view-script-integrate, See equivalent in the Servlet stack>>#
|
||||
|
||||
You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use,
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.scriptTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ScriptTemplateConfigurer configurer() {
|
||||
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
|
||||
configurer.setEngineName("nashorn");
|
||||
configurer.setScripts("mustache.js");
|
||||
configurer.setRenderObject("Mustache");
|
||||
configurer.setRenderFunction("render");
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
----
|
||||
[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:
|
||||
|
||||
* `String template`: The template content
|
||||
* `Map model`: The view model
|
||||
* `RenderingContext renderingContext`: The
|
||||
{api-spring-framework}/web/servlet/view/script/RenderingContext.html[`RenderingContext`]
|
||||
that gives access to the application context, the locale, the template loader, and the
|
||||
URL (since 5.0)
|
||||
|
||||
`Mustache.render()` is natively compatible with this signature, so you can call it directly.
|
||||
|
||||
If your templating technology requires some customization, you can provide a script that
|
||||
implements a custom render function. For example, https://handlebarsjs.com[Handlerbars]
|
||||
needs to compile templates before using them and requires a
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.scriptTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ScriptTemplateConfigurer configurer() {
|
||||
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
|
||||
configurer.setEngineName("nashorn");
|
||||
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
|
||||
configurer.setRenderFunction("render");
|
||||
configurer.setSharedEngine(false);
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
----
|
||||
[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
|
||||
React running on Nashorn. In that case, Java SE 8 update 60 is required, due to
|
||||
https://bugs.openjdk.java.net/browse/JDK-8076099[this bug], but it is generally
|
||||
recommended to use a recent Java SE patch release in any case.
|
||||
|
||||
`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"]
|
||||
----
|
||||
var window = {};
|
||||
----
|
||||
|
||||
This basic `render.js` implementation compiles the template before using it. A production
|
||||
ready implementation should also store and reused cached templates or pre-compiled templates.
|
||||
This can be done on the script side, as well as any customization you need (managing
|
||||
template engine configuration for example).
|
||||
The following example shows how compile a template:
|
||||
|
||||
[source,javascript,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
function render(template, model) {
|
||||
var compiledTemplate = Handlebars.compile(template);
|
||||
return compiledTemplate(model);
|
||||
}
|
||||
----
|
||||
|
||||
Check out the Spring Framework unit tests,
|
||||
{spring-framework-main-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and
|
||||
{spring-framework-main-code}/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script[resources],
|
||||
for more configuration examples.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-view-httpmessagewriter]]
|
||||
== JSON and XML
|
||||
[.small]#<<web.adoc#mvc-view-jackson, See equivalent in the Servlet stack>>#
|
||||
|
||||
For <<webflux-multiple-representations>> purposes, it is useful to be able to alternate
|
||||
between rendering a model with an HTML template or as other formats (such as JSON or XML),
|
||||
depending on the content type requested by the client. To support doing so, Spring WebFlux
|
||||
provides the `HttpMessageWriterView`, which you can use to plug in any of the available
|
||||
<<webflux-codecs>> from `spring-web`, such as `Jackson2JsonEncoder`, `Jackson2SmileEncoder`,
|
||||
or `Jaxb2XmlEncoder`.
|
||||
|
||||
Unlike other view technologies, `HttpMessageWriterView` does not require a `ViewResolver`
|
||||
but is instead <<webflux-config-view-resolvers, configured>> as a default view. You can
|
||||
configure one or more such default views, wrapping different `HttpMessageWriter` instances
|
||||
or `Encoder` instances. The one that matches the requested content type is used at runtime.
|
||||
|
||||
In most cases, a model contains multiple attributes. To determine which one to serialize,
|
||||
you can configure `HttpMessageWriterView` with the name of the model attribute to use for
|
||||
rendering. If the model contains only one attribute, that one is used.
|
||||
1223
framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc
Normal file
1223
framework-docs/modules/ROOT/pages/web/webflux-webclient.adoc
Normal file
File diff suppressed because it is too large
Load Diff
461
framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc
Normal file
461
framework-docs/modules/ROOT/pages/web/webflux-websocket.adoc
Normal file
@@ -0,0 +1,461 @@
|
||||
[[webflux-websocket]]
|
||||
= WebSockets
|
||||
[.small]#<<web.adoc#websocket, See equivalent in the Servlet stack>>#
|
||||
|
||||
This part of the reference documentation covers support for reactive-stack WebSocket
|
||||
messaging.
|
||||
|
||||
include::websocket-intro.adoc[leveloffset=+1]
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server]]
|
||||
== WebSocket API
|
||||
[.small]#<<web.adoc#websocket-server, See equivalent in the Servlet stack>>#
|
||||
|
||||
The Spring Framework provides a WebSocket API that you can use to write client- and
|
||||
server-side applications that handle WebSocket messages.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server-handler]]
|
||||
=== Server
|
||||
[.small]#<<web.adoc#websocket-server-handler, See equivalent in the Servlet stack>>#
|
||||
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||
import org.springframework.web.reactive.socket.WebSocketSession;
|
||||
|
||||
public class MyWebSocketHandler implements WebSocketHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[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<Void> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Then you can map it to a URL:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
@Bean
|
||||
public HandlerMapping handlerMapping() {
|
||||
Map<String, WebSocketHandler> map = new HashMap<>();
|
||||
map.put("/path", new MyWebSocketHandler());
|
||||
int order = -1; // before annotated controllers
|
||||
|
||||
return new SimpleUrlHandlerMapping(map, order);
|
||||
}
|
||||
}
|
||||
----
|
||||
[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)
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
If using the <<web-reactive.adoc#webflux-config, WebFlux Config>> there is nothing
|
||||
further to do, or otherwise if not using the WebFlux config you'll need to declare a
|
||||
`WebSocketHandlerAdapter` as shown below:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
// ...
|
||||
|
||||
@Bean
|
||||
public WebSocketHandlerAdapter handlerAdapter() {
|
||||
return new WebSocketHandlerAdapter();
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
// ...
|
||||
|
||||
@Bean
|
||||
fun handlerAdapter() = WebSocketHandlerAdapter()
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[webflux-websockethandler]]
|
||||
=== `WebSocketHandler`
|
||||
|
||||
The `handle` method of `WebSocketHandler` takes `WebSocketSession` and returns `Mono<Void>`
|
||||
to indicate when application handling of the session is complete. The session is handled
|
||||
through two streams, one for inbound and one for outbound messages. The following table
|
||||
describes the two methods that handle the streams:
|
||||
|
||||
[options="header"]
|
||||
|===
|
||||
| `WebSocketSession` method | Description
|
||||
|
||||
| `Flux<WebSocketMessage> receive()`
|
||||
| Provides access to the inbound message stream and completes when the connection is closed.
|
||||
|
||||
| `Mono<Void> send(Publisher<WebSocketMessage>)`
|
||||
| Takes a source for outgoing messages, writes the messages, and returns a `Mono<Void>` that
|
||||
completes when the source completes and writing is done.
|
||||
|
||||
|===
|
||||
|
||||
A `WebSocketHandler` must compose the inbound and outbound streams into a unified flow and
|
||||
return a `Mono<Void>` that reflects the completion of that flow. Depending on application
|
||||
requirements, the unified flow completes when:
|
||||
|
||||
* Either the inbound or the outbound message stream completes.
|
||||
* The inbound stream completes (that is, the connection closed), while the outbound stream is infinite.
|
||||
* At a chosen point, through the `close` method of `WebSocketSession`.
|
||||
|
||||
When inbound and outbound message streams are composed together, there is no need to
|
||||
check if the connection is open, since Reactive Streams signals end activity.
|
||||
The inbound stream receives a completion or error signal, and the outbound stream
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
class ExampleHandler implements WebSocketHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> 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<Void>` that completes when receiving completes.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class ExampleHandler : WebSocketHandler {
|
||||
|
||||
override fun handle(session: WebSocketSession): Mono<Void> {
|
||||
return session.receive() // <1>
|
||||
.doOnNext {
|
||||
// ... // <2>
|
||||
}
|
||||
.concatMap {
|
||||
// ... // <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<Void>` that completes when receiving completes.
|
||||
|
||||
|
||||
TIP: For nested, asynchronous operations, you may need to call `message.retain()` on underlying
|
||||
servers that use pooled data buffers (for example, Netty). Otherwise, the data buffer may be
|
||||
released before you have had a chance to read the data. For more background, see
|
||||
<<core.adoc#databuffers, Data Buffers and Codecs>>.
|
||||
|
||||
The following implementation combines the inbound and outbound streams:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
class ExampleHandler implements WebSocketHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
|
||||
Flux<WebSocketMessage> output = session.receive() // <1>
|
||||
.doOnNext(message -> {
|
||||
// ...
|
||||
})
|
||||
.concatMap(message -> {
|
||||
// ...
|
||||
})
|
||||
.map(value -> session.textMessage("Echo " + value)); // <2>
|
||||
|
||||
return session.send(output); // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Handle the inbound message stream.
|
||||
<2> Create the outbound message, producing a combined flow.
|
||||
<3> Return a `Mono<Void>` 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<Void> {
|
||||
|
||||
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.
|
||||
<3> Return a `Mono<Void>` that does not complete while we continue to receive.
|
||||
|
||||
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
class ExampleHandler implements WebSocketHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
|
||||
Mono<Void> input = session.receive() <1>
|
||||
.doOnNext(message -> {
|
||||
// ...
|
||||
})
|
||||
.concatMap(message -> {
|
||||
// ...
|
||||
})
|
||||
.then();
|
||||
|
||||
Flux<String> source = ... ;
|
||||
Mono<Void> output = session.send(source.map(session::textMessage)); <2>
|
||||
|
||||
return Mono.zip(input, output).then(); <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Handle inbound message stream.
|
||||
<2> Send outgoing messages.
|
||||
<3> Join the streams and return a `Mono<Void>` 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<Void> {
|
||||
|
||||
val input = session.receive() // <1>
|
||||
.doOnNext {
|
||||
// ...
|
||||
}
|
||||
.concatMap {
|
||||
// ...
|
||||
}
|
||||
.then()
|
||||
|
||||
val source: Flux<String> = ...
|
||||
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.
|
||||
<3> Join the streams and return a `Mono<Void>` that completes when either stream ends.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-databuffer]]
|
||||
=== `DataBuffer`
|
||||
|
||||
`DataBuffer` is the representation for a byte buffer in WebFlux. The Spring Core part of
|
||||
the reference has more on that in the section on
|
||||
<<core#databuffers, Data Buffers and Codecs>>. The key point to understand is that on some
|
||||
servers like Netty, byte buffers are pooled and reference counted, and must be released
|
||||
when consumed to avoid memory leaks.
|
||||
|
||||
When running on Netty, applications must use `DataBufferUtils.retain(dataBuffer)` if they
|
||||
wish to hold on input data buffers in order to ensure they are not released, and
|
||||
subsequently use `DataBufferUtils.release(dataBuffer)` when the buffers are consumed.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server-handshake]]
|
||||
=== Handshake
|
||||
[.small]#<<web.adoc#websocket-server-handshake, See equivalent in the Servlet stack>>#
|
||||
|
||||
`WebSocketHandlerAdapter` delegates to a `WebSocketService`. By default, that is an instance
|
||||
of `HandshakeWebSocketService`, which performs basic checks on the WebSocket request and
|
||||
then uses `RequestUpgradeStrategy` for the server in use. Currently, there is built-in
|
||||
support for Reactor Netty, Tomcat, Jetty, and Undertow.
|
||||
|
||||
`HandshakeWebSocketService` exposes a `sessionAttributePredicate` property that allows
|
||||
setting a `Predicate<String>` to extract attributes from the `WebSession` and insert them
|
||||
into the attributes of the `WebSocketSession`.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server-config]]
|
||||
=== Server Configuration
|
||||
[.small]#<<web.adoc#websocket-server-runtime-configuration, See equivalent in the Servlet stack>>#
|
||||
|
||||
The `RequestUpgradeStrategy` for each server exposes configuration specific to the
|
||||
underlying WebSocket server engine. When using the WebFlux Java config you can customize
|
||||
such properties as shown in the corresponding section of the
|
||||
<<web-reactive.adoc#webflux-config-websocket-service, WebFlux Config>>, or otherwise if
|
||||
not using the WebFlux config, use the below:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
@Bean
|
||||
public WebSocketHandlerAdapter handlerAdapter() {
|
||||
return new WebSocketHandlerAdapter(webSocketService());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketService webSocketService() {
|
||||
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
|
||||
strategy.setMaxSessionIdleTimeout(0L);
|
||||
return new HandshakeWebSocketService(strategy);
|
||||
}
|
||||
}
|
||||
----
|
||||
[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.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server-cors]]
|
||||
=== CORS
|
||||
[.small]#<<web.adoc#websocket-server-allowed-origins, See equivalent in the Servlet stack>>#
|
||||
|
||||
The easiest way to configure CORS and restrict access to a WebSocket endpoint is to
|
||||
have your `WebSocketHandler` implement `CorsConfigurationSource` and return a
|
||||
`CorsConfiguration` with allowed origins, headers, and other details. If you cannot do
|
||||
that, you can also set the `corsConfigurations` property on the `SimpleUrlHandler` to
|
||||
specify CORS settings by URL pattern. If both are specified, they are combined by using the
|
||||
`combine` method on `CorsConfiguration`.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-client]]
|
||||
=== Client
|
||||
|
||||
Spring WebFlux provides a `WebSocketClient` abstraction with implementations for
|
||||
Reactor Netty, Tomcat, Jetty, Undertow, and standard Java (that is, JSR-356).
|
||||
|
||||
NOTE: The Tomcat client is effectively an extension of the standard Java one with some extra
|
||||
functionality in the `WebSocketSession` handling to take advantage of the Tomcat-specific
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
WebSocketClient client = new ReactorNettyWebSocketClient();
|
||||
|
||||
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
|
||||
before you can use them. All clients have constructor options related to configuration
|
||||
of the underlying WebSocket client.
|
||||
4700
framework-docs/modules/ROOT/pages/web/webflux.adoc
Normal file
4700
framework-docs/modules/ROOT/pages/web/webflux.adoc
Normal file
File diff suppressed because it is too large
Load Diff
55
framework-docs/modules/ROOT/pages/web/webmvc-client.adoc
Normal file
55
framework-docs/modules/ROOT/pages/web/webmvc-client.adoc
Normal file
@@ -0,0 +1,55 @@
|
||||
[[webmvc-client]]
|
||||
= REST Clients
|
||||
|
||||
This section describes options for client-side access to REST endpoints.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-resttemplate]]
|
||||
== `RestTemplate`
|
||||
|
||||
`RestTemplate` is a synchronous client to perform HTTP requests. It is the original
|
||||
Spring REST client and exposes a simple, template-method API over underlying HTTP client
|
||||
libraries.
|
||||
|
||||
NOTE: As of 5.0 the `RestTemplate` is in maintenance mode, with only requests for minor
|
||||
changes and bugs to be accepted. Please, consider using the
|
||||
<<web-reactive.adoc#webflux-client, WebClient>> which offers a more modern API and
|
||||
supports sync, async, and streaming scenarios.
|
||||
|
||||
See <<integration.adoc#rest-client-access, REST Endpoints>> for details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was
|
||||
introduced in 5.0 and offers a modern alternative to the `RestTemplate`, with efficient
|
||||
support for both synchronous and asynchronous, as well as streaming scenarios.
|
||||
|
||||
In contrast to `RestTemplate`, `WebClient` supports the following:
|
||||
|
||||
* Non-blocking I/O.
|
||||
* Reactive Streams back pressure.
|
||||
* High concurrency with fewer hardware resources.
|
||||
* Functional-style, fluent API that takes advantage of Java 8 lambdas.
|
||||
* Synchronous and asynchronous interactions.
|
||||
* Streaming up to or streaming down from a server.
|
||||
|
||||
See <<web-reactive.adoc#webflux-client, WebClient>> for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-http-interface]]
|
||||
== HTTP Interface
|
||||
|
||||
The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP
|
||||
exchange methods. You can then generate a proxy that implements this interface and
|
||||
performs the exchanges. This helps to simplify HTTP remote access and provides additional
|
||||
flexibility for to choose an API style such as synchronous or reactive.
|
||||
|
||||
See <<integration.adoc#rest-http-interface, REST Endpoints>> for details.
|
||||
379
framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc
Normal file
379
framework-docs/modules/ROOT/pages/web/webmvc-cors.adoc
Normal file
@@ -0,0 +1,379 @@
|
||||
[[mvc-cors]]
|
||||
= CORS
|
||||
[.small]#<<web-reactive.adoc#webflux-cors, See equivalent in the Reactive stack>>#
|
||||
|
||||
Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
describes how to do so.
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-intro]]
|
||||
== Introduction
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-intro, See equivalent in the Reactive stack>>#
|
||||
|
||||
For security reasons, browsers prohibit AJAX calls to resources outside the current origin.
|
||||
For example, you could have your bank account in one tab and evil.com in another. Scripts
|
||||
from evil.com should not be able to make AJAX requests to your bank API with your
|
||||
credentials -- for example withdrawing money from your account!
|
||||
|
||||
Cross-Origin Resource Sharing (CORS) is a https://www.w3.org/TR/cors/[W3C specification]
|
||||
implemented by https://caniuse.com/#feat=cors[most browsers] that lets you specify
|
||||
what kind of cross-domain requests are authorized, rather than using less secure and less
|
||||
powerful workarounds based on IFRAME or JSONP.
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-processing]]
|
||||
== Processing
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-processing, See equivalent in the Reactive stack>>#
|
||||
|
||||
The CORS specification distinguishes between preflight, simple, and actual requests.
|
||||
To learn how CORS works, you can read
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[this article], among
|
||||
many others, or see the specification for more details.
|
||||
|
||||
Spring MVC `HandlerMapping` implementations provide built-in support for CORS. After successfully
|
||||
mapping a request to a handler, `HandlerMapping` implementations check the CORS configuration for the
|
||||
given request and handler and take further actions. Preflight requests are handled
|
||||
directly, while simple and actual CORS requests are intercepted, validated, and have
|
||||
required CORS response headers set.
|
||||
|
||||
In order to enable cross-origin requests (that is, the `Origin` header is present and
|
||||
differs from the host of the request), you need to have some explicitly declared CORS
|
||||
configuration. If no matching CORS configuration is found, preflight requests are
|
||||
rejected. No CORS headers are added to the responses of simple and actual CORS requests
|
||||
and, consequently, browsers reject them.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{api-spring-framework}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
|
||||
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
|
||||
use the MVC Java configuration or the XML namespace to declare such mappings, which results
|
||||
in a single global map being passed to all `HandlerMapping` instances.
|
||||
|
||||
You can combine global CORS configuration at the `HandlerMapping` level with more
|
||||
fine-grained, handler-level CORS configuration. For example, annotated controllers can use
|
||||
class- or method-level `@CrossOrigin` annotations (other handlers can implement
|
||||
`CorsConfigurationSource`).
|
||||
|
||||
The rules for combining global and local configuration are generally additive -- for example,
|
||||
all global and all local origins. For those attributes where only a single value can be
|
||||
accepted, e.g. `allowCredentials` and `maxAge`, the local overrides the global value. See
|
||||
{api-spring-framework}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
for more details.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
To learn more from the source or make advanced customizations, check the code behind:
|
||||
|
||||
* `CorsConfiguration`
|
||||
* `CorsProcessor`, `DefaultCorsProcessor`
|
||||
* `AbstractHandlerMapping`
|
||||
====
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-controller]]
|
||||
== `@CrossOrigin`
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-controller, See equivalent in the Reactive stack>>#
|
||||
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin
|
||||
@GetMapping("/{id}")
|
||||
public Account retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
class AccountController {
|
||||
|
||||
@CrossOrigin
|
||||
@GetMapping("/{id}")
|
||||
fun retrieve(@PathVariable id: Long): Account {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
By default, `@CrossOrigin` allows:
|
||||
|
||||
* All origins.
|
||||
* All headers.
|
||||
* All HTTP methods to which the controller method is mapped.
|
||||
|
||||
`allowCredentials` is not enabled by default, since that establishes a trust level
|
||||
that exposes sensitive user-specific information (such as cookies and CSRF tokens) and
|
||||
should only be used where appropriate. When it is enabled either `allowOrigins` must be
|
||||
set to one or more specific domain (but not the special value `"*"`) or alternatively
|
||||
the `allowOriginPatterns` property may be used to match to a dynamic set of origins.
|
||||
|
||||
`maxAge` is set to 30 minutes.
|
||||
|
||||
`@CrossOrigin` is supported at the class level, too, and is inherited by all methods,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Account retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@CrossOrigin(origins = ["https://domain2.com"], maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
class AccountController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun retrieve(@PathVariable id: Long): Account {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
You can use `@CrossOrigin` at both the class level and the method level,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@CrossOrigin(maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin("https://domain2.com")
|
||||
@GetMapping("/{id}")
|
||||
public Account retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@CrossOrigin(maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
class AccountController {
|
||||
|
||||
@CrossOrigin("https://domain2.com")
|
||||
@GetMapping("/{id}")
|
||||
fun retrieve(@PathVariable id: Long): Account {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-global]]
|
||||
== Global Configuration
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-global, See equivalent in the Reactive stack>>#
|
||||
|
||||
In addition to fine-grained, controller method level configuration, you probably want to
|
||||
define some global CORS configuration, too. You can set URL-based `CorsConfiguration`
|
||||
mappings individually on any `HandlerMapping`. Most applications, however, use the
|
||||
MVC Java configuration or the MVC XML namespace to do that.
|
||||
|
||||
By default, global configuration enables the following:
|
||||
|
||||
* All origins.
|
||||
* All headers.
|
||||
* `GET`, `HEAD`, and `POST` methods.
|
||||
|
||||
|
||||
`allowCredentials` is not enabled by default, since that establishes a trust level
|
||||
that exposes sensitive user-specific information (such as cookies and CSRF tokens) and
|
||||
should only be used where appropriate. When it is enabled either `allowOrigins` must be
|
||||
set to one or more specific domain (but not the special value `"*"`) or alternatively
|
||||
the `allowOriginPatterns` property may be used to match to a dynamic set of origins.
|
||||
|
||||
`maxAge` is set to 30 minutes.
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-global-java]]
|
||||
=== Java Configuration
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-global, See equivalent in the Reactive stack>>#
|
||||
|
||||
To enable CORS in the MVC Java config, you can use the `CorsRegistry` callback,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@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);
|
||||
|
||||
// Add more mappings...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
class WebConfig : WebMvcConfigurer {
|
||||
|
||||
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...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-global-xml]]
|
||||
=== XML Configuration
|
||||
|
||||
To enable CORS in the XML namespace, you can use the `<mvc:cors>` element,
|
||||
as the following example shows:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim"]
|
||||
----
|
||||
<mvc:cors>
|
||||
|
||||
<mvc:mapping path="/api/**"
|
||||
allowed-origins="https://domain1.com, https://domain2.com"
|
||||
allowed-methods="GET, PUT"
|
||||
allowed-headers="header1, header2, header3"
|
||||
exposed-headers="header1, header2" allow-credentials="true"
|
||||
max-age="123" />
|
||||
|
||||
<mvc:mapping path="/resources/**"
|
||||
allowed-origins="https://domain1.com" />
|
||||
|
||||
</mvc:cors>
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-filter]]
|
||||
== CORS Filter
|
||||
[.small]#<<webflux-cors.adoc#webflux-cors-webfilter, See equivalent in the Reactive stack>>#
|
||||
|
||||
You can apply CORS support through the built-in
|
||||
{api-spring-framework}/web/filter/CorsFilter.html[`CorsFilter`].
|
||||
|
||||
NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring
|
||||
Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for
|
||||
CORS.
|
||||
|
||||
To configure the filter, pass a `CorsConfigurationSource` to its constructor, as the
|
||||
following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim",role="primary"]
|
||||
.Java
|
||||
----
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
|
||||
// Possibly...
|
||||
// config.applyPermitDefaultValues()
|
||||
|
||||
config.setAllowCredentials(true);
|
||||
config.addAllowedOrigin("https://domain1.com");
|
||||
config.addAllowedHeader("*");
|
||||
config.addAllowedMethod("*");
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
CorsFilter filter = new CorsFilter(source);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val config = CorsConfiguration()
|
||||
|
||||
// Possibly...
|
||||
// config.applyPermitDefaultValues()
|
||||
|
||||
config.allowCredentials = true
|
||||
config.addAllowedOrigin("https://domain1.com")
|
||||
config.addAllowedHeader("*")
|
||||
config.addAllowedMethod("*")
|
||||
|
||||
val source = UrlBasedCorsConfigurationSource()
|
||||
source.registerCorsConfiguration("/**", config)
|
||||
|
||||
val filter = CorsFilter(source)
|
||||
----
|
||||
881
framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc
Normal file
881
framework-docs/modules/ROOT/pages/web/webmvc-functional.adoc
Normal file
@@ -0,0 +1,881 @@
|
||||
[[webmvc-fn]]
|
||||
= Functional Endpoints
|
||||
[.small]#<<web-reactive.adoc#webflux-fn, See equivalent in the Reactive stack>>#
|
||||
|
||||
Spring Web MVC includes WebMvc.fn, a lightweight functional programming model in which functions
|
||||
are used to route and handle requests and contracts are designed for immutability.
|
||||
It is an alternative to the annotation-based programming model but otherwise runs on
|
||||
the same <<web#mvc-servlet>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-overview]]
|
||||
== Overview
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-overview, See equivalent in the Reactive stack>>#
|
||||
|
||||
In WebMvc.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes
|
||||
`ServerRequest` and returns a `ServerResponse`.
|
||||
Both the request and the response object have immutable contracts that offer JDK 8-friendly
|
||||
access to the HTTP request and response.
|
||||
`HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the
|
||||
annotation-based programming model.
|
||||
|
||||
Incoming requests are routed to a handler function with a `RouterFunction`: a function that
|
||||
takes `ServerRequest` and returns an optional `HandlerFunction` (i.e. `Optional<HandlerFunction>`).
|
||||
When the router function matches, a handler function is returned; otherwise an empty Optional.
|
||||
`RouterFunction` is the equivalent of a `@RequestMapping` annotation, but with the major
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.servlet.function.RequestPredicates.*;
|
||||
import static org.springframework.web.servlet.function.RouterFunctions.route;
|
||||
|
||||
PersonRepository repository = ...
|
||||
PersonHandler handler = new PersonHandler(repository);
|
||||
|
||||
RouterFunction<ServerResponse> route = route() // <1>
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST("/person", handler::createPerson)
|
||||
.build();
|
||||
|
||||
|
||||
public class PersonHandler {
|
||||
|
||||
// ...
|
||||
|
||||
public ServerResponse listPeople(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public ServerResponse createPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public ServerResponse getPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using `route()`.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val repository: PersonRepository = ...
|
||||
val handler = PersonHandler(repository)
|
||||
|
||||
val route = router { // <1>
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/person/{id}", handler::getPerson)
|
||||
GET("/person", handler::listPeople)
|
||||
}
|
||||
POST("/person", handler::createPerson)
|
||||
}
|
||||
|
||||
|
||||
class PersonHandler(private val repository: PersonRepository) {
|
||||
|
||||
// ...
|
||||
|
||||
fun listPeople(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
|
||||
fun createPerson(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
|
||||
fun getPerson(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using the router DSL.
|
||||
|
||||
|
||||
If you register the `RouterFunction` as a bean, for instance by exposing it in a
|
||||
`@Configuration` class, it will be auto-detected by the servlet, as explained in <<webmvc-fn-running>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-handler-functions]]
|
||||
== HandlerFunction
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-handler-functions, See equivalent in the Reactive stack>>#
|
||||
|
||||
`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly
|
||||
access to the HTTP request and response, including headers, body, method, and status code.
|
||||
|
||||
|
||||
[[webmvc-fn-request]]
|
||||
=== ServerRequest
|
||||
|
||||
`ServerRequest` provides access to the HTTP method, URI, headers, and query parameters,
|
||||
while access to the body is provided through the `body` methods.
|
||||
|
||||
The following example extracts the request body to a `String`:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
String string = request.body(String.class);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val string = request.body<String>()
|
||||
----
|
||||
|
||||
|
||||
The following example extracts the body to a `List<Person>`,
|
||||
where `Person` objects are decoded from a serialized form, such as JSON or XML:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val people = request.body<Person>()
|
||||
----
|
||||
|
||||
The following example shows how to access parameters:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
MultiValueMap<String, String> params = request.params();
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val map = request.params()
|
||||
----
|
||||
|
||||
|
||||
[[webmvc-fn-response]]
|
||||
=== ServerResponse
|
||||
|
||||
`ServerResponse` provides access to the HTTP response and, since it is immutable, you can use
|
||||
a `build` method to create it. You can use the builder to set the response status, to add response
|
||||
headers, or to provide a body. The following example creates a 200 (OK) response with JSON
|
||||
content:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Person person = ...
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val person: Person = ...
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
|
||||
----
|
||||
|
||||
The following example shows how to build a 201 (CREATED) response with a `Location` header and no body:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI location = ...
|
||||
ServerResponse.created(location).build();
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val location: URI = ...
|
||||
ServerResponse.created(location).build()
|
||||
----
|
||||
|
||||
You can also use an asynchronous result as the body, in the form of a `CompletableFuture`,
|
||||
`Publisher`, or any other type supported by the `ReactiveAdapterRegistry`. For instance:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val person = webClient.get().retrieve().awaitBody<Person>()
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
|
||||
----
|
||||
|
||||
If not just the body, but also the status or headers are based on an asynchronous type,
|
||||
you can use the static `async` method on `ServerResponse`, which
|
||||
accepts `CompletableFuture<ServerResponse>`, `Publisher<ServerResponse>`, or
|
||||
any other asynchronous type supported by the `ReactiveAdapterRegistry`. For instance:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
|
||||
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
|
||||
ServerResponse.async(asyncResponse);
|
||||
----
|
||||
|
||||
https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the
|
||||
static `sse` method on `ServerResponse`. The builder provided by that method
|
||||
allows you to send Strings, or other objects as JSON. For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
public RouterFunction<ServerResponse> sse() {
|
||||
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
|
||||
// Save the sseBuilder object somewhere..
|
||||
}));
|
||||
}
|
||||
|
||||
// In some other thread, sending a String
|
||||
sseBuilder.send("Hello world");
|
||||
|
||||
// Or an object, which will be transformed into JSON
|
||||
Person person = ...
|
||||
sseBuilder.send(person);
|
||||
|
||||
// Customize the event by using the other methods
|
||||
sseBuilder.id("42")
|
||||
.event("sse event")
|
||||
.data(person);
|
||||
|
||||
// and done at some point
|
||||
sseBuilder.complete();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
fun sse(): RouterFunction<ServerResponse> = router {
|
||||
GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
|
||||
// Save the sseBuilder object somewhere..
|
||||
}
|
||||
}
|
||||
|
||||
// In some other thread, sending a String
|
||||
sseBuilder.send("Hello world")
|
||||
|
||||
// Or an object, which will be transformed into JSON
|
||||
val person = ...
|
||||
sseBuilder.send(person)
|
||||
|
||||
// Customize the event by using the other methods
|
||||
sseBuilder.id("42")
|
||||
.event("sse event")
|
||||
.data(person)
|
||||
|
||||
// and done at some point
|
||||
sseBuilder.complete()
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-handler-classes]]
|
||||
=== Handler Classes
|
||||
|
||||
We can write a handler function as a lambda, as the following example shows:
|
||||
|
||||
--
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
HandlerFunction<ServerResponse> helloWorld =
|
||||
request -> ServerResponse.ok().body("Hello World");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val helloWorld: (ServerRequest) -> ServerResponse =
|
||||
{ ServerResponse.ok().body("Hello World") }
|
||||
----
|
||||
--
|
||||
|
||||
That is convenient, but in an application we need multiple functions, and multiple inline
|
||||
lambda's can get messy.
|
||||
Therefore, it is useful to group related handler functions together into a handler class, which
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
|
||||
|
||||
public class PersonHandler {
|
||||
|
||||
private final PersonRepository repository;
|
||||
|
||||
public PersonHandler(PersonRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public ServerResponse listPeople(ServerRequest request) { // <1>
|
||||
List<Person> people = repository.allPeople();
|
||||
return ok().contentType(APPLICATION_JSON).body(people);
|
||||
}
|
||||
|
||||
public ServerResponse createPerson(ServerRequest request) throws Exception { // <2>
|
||||
Person person = request.body(Person.class);
|
||||
repository.savePerson(person);
|
||||
return ok().build();
|
||||
}
|
||||
|
||||
public ServerResponse getPerson(ServerRequest request) { // <3>
|
||||
int personId = Integer.parseInt(request.pathVariable("id"));
|
||||
Person person = repository.getPerson(personId);
|
||||
if (person != null) {
|
||||
return ok().contentType(APPLICATION_JSON).body(person);
|
||||
}
|
||||
else {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
<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.
|
||||
<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.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class PersonHandler(private val repository: PersonRepository) {
|
||||
|
||||
fun listPeople(request: ServerRequest): ServerResponse { // <1>
|
||||
val people: List<Person> = repository.allPeople()
|
||||
return ok().contentType(APPLICATION_JSON).body(people);
|
||||
}
|
||||
|
||||
fun createPerson(request: ServerRequest): ServerResponse { // <2>
|
||||
val person = request.body<Person>()
|
||||
repository.savePerson(person)
|
||||
return ok().build()
|
||||
}
|
||||
|
||||
fun getPerson(request: ServerRequest): ServerResponse { // <3>
|
||||
val personId = request.pathVariable("id").toInt()
|
||||
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
|
||||
?: ServerResponse.notFound().build()
|
||||
|
||||
}
|
||||
}
|
||||
----
|
||||
<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.
|
||||
<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.
|
||||
--
|
||||
|
||||
|
||||
[[webmvc-fn-handler-validation]]
|
||||
=== Validation
|
||||
|
||||
A functional endpoint can use Spring's <<core.adoc#validation, validation facilities>> to
|
||||
apply validation to the request body. For example, given a custom Spring
|
||||
<<core.adoc#validation, Validator>> implementation for a `Person`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
public class PersonHandler {
|
||||
|
||||
private final Validator validator = new PersonValidator(); // <1>
|
||||
|
||||
// ...
|
||||
|
||||
public ServerResponse createPerson(ServerRequest request) {
|
||||
Person person = request.body(Person.class);
|
||||
validate(person); // <2>
|
||||
repository.savePerson(person);
|
||||
return ok().build();
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
// ...
|
||||
|
||||
fun createPerson(request: ServerRequest): ServerResponse {
|
||||
val person = request.body<Person>()
|
||||
validate(person) // <2>
|
||||
repository.savePerson(person)
|
||||
return ok().build()
|
||||
}
|
||||
|
||||
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`.
|
||||
See <<core.adoc#validation-beanvalidation, Spring Validation>>.
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-router-functions]]
|
||||
== `RouterFunction`
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-router-functions, See equivalent in the Reactive stack>>#
|
||||
|
||||
Router functions are used to route the requests to the corresponding `HandlerFunction`.
|
||||
Typically, you do not write router functions yourself, but rather use a method on the
|
||||
`RouterFunctions` utility class to create one.
|
||||
`RouterFunctions.route()` (no parameters) provides you with a fluent builder for creating a router
|
||||
function, whereas `RouterFunctions.route(RequestPredicate, HandlerFunction)` offers a direct way
|
||||
to create a router.
|
||||
|
||||
Generally, it is recommended to use the `route()` builder, as it provides
|
||||
convenient short-cuts for typical mapping scenarios without requiring hard-to-discover
|
||||
static imports.
|
||||
For instance, the router function builder offers the method `GET(String, HandlerFunction)` to create a mapping for GET requests; and `POST(String, HandlerFunction)` for POSTs.
|
||||
|
||||
Besides HTTP method-based mapping, the route builder offers a way to introduce additional
|
||||
predicates when mapping to requests.
|
||||
For each HTTP method there is an overloaded variant that takes a `RequestPredicate` as a
|
||||
parameter, through which additional constraints can be expressed.
|
||||
|
||||
|
||||
[[webmvc-fn-predicates]]
|
||||
=== Predicates
|
||||
|
||||
You can write your own `RequestPredicate`, but the `RequestPredicates` utility class
|
||||
offers commonly used implementations, based on the request path, HTTP method, content-type,
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = RouterFunctions.route()
|
||||
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
|
||||
request -> ServerResponse.ok().body("Hello World")).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
GET("/hello-world", accept(TEXT_PLAIN)) {
|
||||
ServerResponse.ok().body("Hello World")
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
You can compose multiple request predicates together by using:
|
||||
|
||||
* `RequestPredicate.and(RequestPredicate)` -- both must match.
|
||||
* `RequestPredicate.or(RequestPredicate)` -- either can match.
|
||||
|
||||
Many of the predicates from `RequestPredicates` are composed.
|
||||
For example, `RequestPredicates.GET(String)` is composed from `RequestPredicates.method(HttpMethod)`
|
||||
and `RequestPredicates.path(String)`.
|
||||
The example shown above also uses two request predicates, as the builder uses
|
||||
`RequestPredicates.GET` internally, and composes that with the `accept` predicate.
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-routes]]
|
||||
=== Routes
|
||||
|
||||
Router functions are evaluated in order: if the first route does not match, the
|
||||
second is evaluated, and so on.
|
||||
Therefore, it makes sense to declare more specific routes before general ones.
|
||||
This is also important when registering router functions as Spring beans, as will
|
||||
be described later.
|
||||
Note that this behavior is different from the annotation-based programming model, where the
|
||||
"most specific" controller method is picked automatically.
|
||||
|
||||
When using the router function builder, all defined routes are composed into one
|
||||
`RouterFunction` that is returned from `build()`.
|
||||
There are also other ways to compose multiple router functions together:
|
||||
|
||||
* `add(RouterFunction)` on the `RouterFunctions.route()` builder
|
||||
* `RouterFunction.and(RouterFunction)`
|
||||
* `RouterFunction.andRoute(RequestPredicate, HandlerFunction)` -- shortcut for
|
||||
`RouterFunction.and()` with nested `RouterFunctions.route()`.
|
||||
|
||||
The following example shows the composition of four routes:
|
||||
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.servlet.function.RequestPredicates.*;
|
||||
|
||||
PersonRepository repository = ...
|
||||
PersonHandler handler = new PersonHandler(repository);
|
||||
|
||||
RouterFunction<ServerResponse> otherRoute = ...
|
||||
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
|
||||
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
|
||||
.POST("/person", handler::createPerson) // <3>
|
||||
.add(otherRoute) // <4>
|
||||
.build();
|
||||
----
|
||||
<1> pass:q[`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.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.http.MediaType.APPLICATION_JSON
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val repository: PersonRepository = ...
|
||||
val handler = PersonHandler(repository);
|
||||
|
||||
val otherRoute = router { }
|
||||
|
||||
val route = router {
|
||||
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
|
||||
GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
|
||||
POST("/person", handler::createPerson) // <3>
|
||||
}.and(otherRoute) // <4>
|
||||
----
|
||||
<1> pass:q[`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
|
||||
|
||||
It is common for a group of router functions to have a shared predicate, for instance a shared
|
||||
path.
|
||||
In the example above, the shared predicate would be a path predicate that matches `/person`,
|
||||
used by three of the routes.
|
||||
When using annotations, you would remove this duplication by using a type-level `@RequestMapping`
|
||||
annotation that maps to `/person`.
|
||||
In WebMvc.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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", builder -> builder // <1>
|
||||
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST(handler::createPerson))
|
||||
.build();
|
||||
----
|
||||
<1> Note that second parameter of `path` is a consumer that takes the router builder.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
"/person".nest { // <1>
|
||||
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Using `nest` DSL.
|
||||
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(handler::createPerson))
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
"/person".nest {
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[[webmvc-fn-running]]
|
||||
== Running a Server
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-running, See equivalent in the Reactive stack>>#
|
||||
|
||||
You typically run router functions in a <<web.adoc#mvc-servlet, `DispatcherHandler`>>-based setup through the
|
||||
<<web.adoc#mvc-config>>, which uses Spring configuration to declare the
|
||||
components required to process requests. The MVC Java configuration declares the following
|
||||
infrastructure components to support functional endpoints:
|
||||
|
||||
* `RouterFunctionMapping`: Detects one or more `RouterFunction<?>` beans in the Spring
|
||||
configuration, <<core.adoc#beans-factory-ordered, orders them>>, combines them through
|
||||
`RouterFunction.andOther`, and routes requests to the resulting composed `RouterFunction`.
|
||||
* `HandlerFunctionAdapter`: Simple adapter that lets `DispatcherHandler` invoke
|
||||
a `HandlerFunction` that was mapped to a request.
|
||||
|
||||
The preceding components let functional endpoints fit within the `DispatcherServlet` request
|
||||
processing lifecycle and also (potentially) run side by side with annotated controllers, if
|
||||
any are declared. It is also how functional endpoints are enabled by the Spring Boot Web
|
||||
starter.
|
||||
|
||||
The following example shows a WebFlux Java configuration:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableMvc
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public RouterFunction<?> routerFunctionA() {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<?> routerFunctionB() {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
// 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
|
||||
@EnableMvc
|
||||
class WebConfig : WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
fun routerFunctionA(): RouterFunction<*> {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun routerFunctionB(): RouterFunction<*> {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
|
||||
// configure message conversion...
|
||||
}
|
||||
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
// configure CORS...
|
||||
}
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
// configure view resolution for HTML rendering...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-handler-filter-function]]
|
||||
== Filtering Handler Functions
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-handler-filter-function, See equivalent in the Reactive stack>>#
|
||||
|
||||
You can filter handler functions by using the `before`, `after`, or `filter` methods on the routing
|
||||
function builder.
|
||||
With annotations, you can achieve similar functionality by using `@ControllerAdvice`, a `ServletFilter`, or both.
|
||||
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",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople)
|
||||
.before(request -> ServerRequest.from(request) // <1>
|
||||
.header("X-RequestHeader", "Value")
|
||||
.build()))
|
||||
.POST(handler::createPerson))
|
||||
.after((request, response) -> logResponse(response)) // <2>
|
||||
.build();
|
||||
----
|
||||
<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
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
"/person".nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET(handler::listPeople)
|
||||
before { // <1>
|
||||
ServerRequest.from(it)
|
||||
.header("X-RequestHeader", "Value").build()
|
||||
}
|
||||
}
|
||||
POST(handler::createPerson)
|
||||
after { _, response -> // <2>
|
||||
logResponse(response)
|
||||
}
|
||||
}
|
||||
----
|
||||
<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.
|
||||
|
||||
|
||||
The `filter` method on the router builder takes a `HandlerFilterFunction`: a
|
||||
function that takes a `ServerRequest` and `HandlerFunction` and returns a `ServerResponse`.
|
||||
The handler function parameter represents the next element in the chain.
|
||||
This is typically the handler that is routed to, but it can also be another
|
||||
filter if multiple are applied.
|
||||
|
||||
Now we can add a simple security filter to our route, assuming that we have a `SecurityManager` that
|
||||
can determine whether a particular path is allowed.
|
||||
The following example shows how to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
SecurityManager securityManager = ...
|
||||
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(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
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val securityManager: SecurityManager = ...
|
||||
|
||||
val route = router {
|
||||
("/person" and accept(APPLICATION_JSON)).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
filter { request, next ->
|
||||
if (securityManager.allowAccessTo(request.path())) {
|
||||
next(request)
|
||||
}
|
||||
else {
|
||||
status(UNAUTHORIZED).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional.
|
||||
We only let the handler function be run when access is allowed.
|
||||
|
||||
Besides using the `filter` method on the router function builder, it is possible to apply a
|
||||
filter to an existing router function via `RouterFunction.filter(HandlerFilterFunction)`.
|
||||
|
||||
NOTE: CORS support for functional endpoints is provided through a dedicated
|
||||
<<webmvc-cors.adoc#mvc-cors-filter, `CorsFilter`>>.
|
||||
28
framework-docs/modules/ROOT/pages/web/webmvc-test.adoc
Normal file
28
framework-docs/modules/ROOT/pages/web/webmvc-test.adoc
Normal file
@@ -0,0 +1,28 @@
|
||||
[[webmvc.test]]
|
||||
= Testing
|
||||
[.small]#<<web-reactive.adoc#webflux-test, See equivalent in the Reactive stack>>#
|
||||
|
||||
This section summarizes the options available in `spring-test` for Spring MVC applications.
|
||||
|
||||
* Servlet API Mocks: Mock implementations of Servlet API contracts for unit testing controllers,
|
||||
filters, and other web components. See <<testing.adoc#mock-objects-servlet, Servlet API>>
|
||||
mock objects for more details.
|
||||
|
||||
* TestContext Framework: Support for loading Spring configuration in JUnit and TestNG tests,
|
||||
including efficient caching of the loaded configuration across test methods and support for
|
||||
loading a `WebApplicationContext` with a `MockServletContext`.
|
||||
See <<testing.adoc#testcontext-framework,TestContext Framework>> for more details.
|
||||
|
||||
* Spring MVC Test: A framework, also known as `MockMvc`, for testing annotated controllers
|
||||
through the `DispatcherServlet` (that is, supporting annotations), complete with the
|
||||
Spring MVC infrastructure but without an HTTP server.
|
||||
See <<testing.adoc#spring-mvc-test-framework, Spring MVC Test>> for more details.
|
||||
|
||||
* Client-side REST: `spring-test` provides a `MockRestServiceServer` that you can use as
|
||||
a mock server for testing client-side code that internally uses the `RestTemplate`.
|
||||
See <<testing.adoc#spring-mvc-test-client, Client REST Tests>> for more details.
|
||||
|
||||
* `WebTestClient`: Built for testing WebFlux applications, but it can also be used for
|
||||
end-to-end integration testing, to any server, over an HTTP connection. It is a
|
||||
non-blocking, reactive client and is well suited for testing asynchronous and streaming
|
||||
scenarios. See <<testing.adoc#webtestclient, `WebTestClient`>> for more details.
|
||||
2074
framework-docs/modules/ROOT/pages/web/webmvc-view.adoc
Normal file
2074
framework-docs/modules/ROOT/pages/web/webmvc-view.adoc
Normal file
File diff suppressed because it is too large
Load Diff
6344
framework-docs/modules/ROOT/pages/web/webmvc.adoc
Normal file
6344
framework-docs/modules/ROOT/pages/web/webmvc.adoc
Normal file
File diff suppressed because it is too large
Load Diff
103
framework-docs/modules/ROOT/pages/web/websocket-intro.adoc
Normal file
103
framework-docs/modules/ROOT/pages/web/websocket-intro.adoc
Normal file
@@ -0,0 +1,103 @@
|
||||
[id={chapter}.websocket-intro]
|
||||
= Introduction to WebSocket
|
||||
|
||||
The WebSocket protocol, https://tools.ietf.org/html/rfc6455[RFC 6455], provides a standardized
|
||||
way to establish a full-duplex, two-way communication channel between client and server
|
||||
over a single TCP connection. It is a different TCP protocol from HTTP but is designed to
|
||||
work over HTTP, using ports 80 and 443 and allowing re-use of existing firewall rules.
|
||||
|
||||
A WebSocket interaction begins with an HTTP request that uses the HTTP `Upgrade` header
|
||||
to upgrade or, in this case, to switch to the WebSocket protocol. The following example
|
||||
shows such an interaction:
|
||||
|
||||
[source,yaml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
GET /spring-websocket-portfolio/portfolio HTTP/1.1
|
||||
Host: localhost:8080
|
||||
Upgrade: websocket <1>
|
||||
Connection: Upgrade <2>
|
||||
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
|
||||
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
|
||||
Sec-WebSocket-Version: 13
|
||||
Origin: http://localhost:8080
|
||||
----
|
||||
<1> The `Upgrade` header.
|
||||
<2> Using the `Upgrade` connection.
|
||||
|
||||
|
||||
Instead of the usual 200 status code, a server with WebSocket support returns output
|
||||
similar to the following:
|
||||
|
||||
[source,yaml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
HTTP/1.1 101 Switching Protocols <1>
|
||||
Upgrade: websocket
|
||||
Connection: Upgrade
|
||||
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
|
||||
Sec-WebSocket-Protocol: v10.stomp
|
||||
----
|
||||
<1> Protocol switch
|
||||
|
||||
|
||||
After a successful handshake, the TCP socket underlying the HTTP upgrade request remains
|
||||
open for both the client and the server to continue to send and receive messages.
|
||||
|
||||
A complete introduction of how WebSockets work is beyond the scope of this document.
|
||||
See RFC 6455, the WebSocket chapter of HTML5, or any of the many introductions and
|
||||
tutorials on the Web.
|
||||
|
||||
Note that, if a WebSocket server is running behind a web server (e.g. nginx), you
|
||||
likely need to configure it to pass WebSocket upgrade requests on to the WebSocket
|
||||
server. Likewise, if the application runs in a cloud environment, check the
|
||||
instructions of the cloud provider related to WebSocket support.
|
||||
|
||||
|
||||
|
||||
|
||||
[id={chapter}.websocket-intro-architecture]
|
||||
== HTTP Versus WebSocket
|
||||
|
||||
Even though WebSocket is designed to be HTTP-compatible and starts with an HTTP request,
|
||||
it is important to understand that the two protocols lead to very different
|
||||
architectures and application programming models.
|
||||
|
||||
In HTTP and REST, an application is modeled as many URLs. To interact with the application,
|
||||
clients access those URLs, request-response style. Servers route requests to the
|
||||
appropriate handler based on the HTTP URL, method, and headers.
|
||||
|
||||
By contrast, in WebSockets, there is usually only one URL for the initial connect.
|
||||
Subsequently, all application messages flow on that same TCP connection. This points to
|
||||
an entirely different asynchronous, event-driven, messaging architecture.
|
||||
|
||||
WebSocket is also a low-level transport protocol, which, unlike HTTP, does not prescribe
|
||||
any semantics to the content of messages. That means that there is no way to route or process
|
||||
a message unless the client and the server agree on message semantics.
|
||||
|
||||
WebSocket clients and servers can negotiate the use of a higher-level, messaging protocol
|
||||
(for example, STOMP), through the `Sec-WebSocket-Protocol` header on the HTTP handshake request.
|
||||
In the absence of that, they need to come up with their own conventions.
|
||||
|
||||
|
||||
|
||||
|
||||
[id={chapter}.websocket-intro-when-to-use]
|
||||
== When to Use WebSockets
|
||||
|
||||
WebSockets can make a web page be dynamic and interactive. However, in many cases,
|
||||
a combination of AJAX and HTTP streaming or long polling can provide a simple and
|
||||
effective solution.
|
||||
|
||||
For example, news, mail, and social feeds need to update dynamically, but it may be
|
||||
perfectly okay to do so every few minutes. Collaboration, games, and financial apps, on
|
||||
the other hand, need to be much closer to real-time.
|
||||
|
||||
Latency alone is not a deciding factor. If the volume of messages is relatively low (for example,
|
||||
monitoring network failures) HTTP streaming or polling can provide an effective solution.
|
||||
It is the combination of low latency, high frequency, and high volume that make the best
|
||||
case for the use of WebSocket.
|
||||
|
||||
Keep in mind also that over the Internet, restrictive proxies that are outside of your control
|
||||
may preclude WebSocket interactions, either because they are not configured to pass on the
|
||||
`Upgrade` header or because they close long-lived connections that appear idle. This
|
||||
means that the use of WebSocket for internal applications within the firewall is a more
|
||||
straightforward decision than it is for public facing applications.
|
||||
2513
framework-docs/modules/ROOT/pages/web/websocket.adoc
Normal file
2513
framework-docs/modules/ROOT/pages/web/websocket.adoc
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user