Consistent use of tabs for sample code in the reference documentation
This commit is contained in:
parent
08c78554b9
commit
6f24c0de17
|
|
@ -4,6 +4,7 @@
|
|||
:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework
|
||||
:toc: left
|
||||
:toclevels: 4
|
||||
:tabsize: 4
|
||||
:docinfo1:
|
||||
|
||||
This part of the reference documentation covers all of those technologies that are
|
||||
|
|
|
|||
|
|
@ -309,23 +309,23 @@ Typically, such configuration will live in a ".groovy" file with a structure as
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
beans {
|
||||
dataSource(BasicDataSource) {
|
||||
driverClassName = "org.hsqldb.jdbcDriver"
|
||||
url = "jdbc:hsqldb:mem:grailsDB"
|
||||
username = "sa"
|
||||
password = ""
|
||||
settings = [mynew:"setting"]
|
||||
}
|
||||
sessionFactory(SessionFactory) {
|
||||
dataSource = dataSource
|
||||
}
|
||||
myService(MyService) {
|
||||
nestedBean = { AnotherBean bean ->
|
||||
dataSource = dataSource
|
||||
}
|
||||
}
|
||||
}
|
||||
beans {
|
||||
dataSource(BasicDataSource) {
|
||||
driverClassName = "org.hsqldb.jdbcDriver"
|
||||
url = "jdbc:hsqldb:mem:grailsDB"
|
||||
username = "sa"
|
||||
password = ""
|
||||
settings = [mynew:"setting"]
|
||||
}
|
||||
sessionFactory(SessionFactory) {
|
||||
dataSource = dataSource
|
||||
}
|
||||
myService(MyService) {
|
||||
nestedBean = { AnotherBean bean ->
|
||||
dataSource = dataSource
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
This configuration style is largely equivalent to XML bean definitions and even
|
||||
|
|
@ -373,7 +373,7 @@ delegates, e.g. with `XmlBeanDefinitionReader` for XML files:
|
|||
----
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
|
||||
context.refresh();
|
||||
context.refresh();
|
||||
----
|
||||
|
||||
Or with `GroovyBeanDefinitionReader` for Groovy files:
|
||||
|
|
@ -383,7 +383,7 @@ Or with `GroovyBeanDefinitionReader` for Groovy files:
|
|||
----
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
|
||||
context.refresh();
|
||||
context.refresh();
|
||||
----
|
||||
|
||||
Such reader delegates can be mixed and matched on the same `ApplicationContext`,
|
||||
|
|
@ -1685,7 +1685,7 @@ The preceding example is equivalent to the following Java code:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
exampleBean.setEmail("")
|
||||
exampleBean.setEmail("")
|
||||
----
|
||||
|
||||
The `<null/>` element handles `null` values. For example:
|
||||
|
|
@ -3822,7 +3822,6 @@ Find below the custom `BeanPostProcessor` implementation class definition:
|
|||
package scripting;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.beans.BeansException;
|
||||
|
||||
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
|
|
@ -5400,7 +5399,7 @@ comma/semicolon/space-separated list that includes the parent package of each cl
|
|||
@Configuration
|
||||
@ComponentScan(basePackages = "org.example")
|
||||
public class AppConfig {
|
||||
...
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -5511,12 +5510,12 @@ and using "stub" repositories instead.
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = "org.example",
|
||||
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
|
||||
excludeFilters = @Filter(Repository.class))
|
||||
public class AppConfig {
|
||||
...
|
||||
}
|
||||
@ComponentScan(basePackages = "org.example",
|
||||
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
|
||||
excludeFilters = @Filter(Repository.class))
|
||||
public class AppConfig {
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
and the equivalent using XML
|
||||
|
|
@ -5746,10 +5745,10 @@ fully-qualified class name when configuring the scanner:
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
|
||||
public class AppConfig {
|
||||
...
|
||||
}
|
||||
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
|
||||
public class AppConfig {
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
[source,xml,indent=0]
|
||||
|
|
@ -5803,8 +5802,8 @@ fully-qualified class name when configuring the scanner:
|
|||
@Configuration
|
||||
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
|
||||
public class AppConfig {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
[source,xml,indent=0]
|
||||
|
|
@ -5828,8 +5827,8 @@ the following configuration will result in standard JDK dynamic proxies:
|
|||
@Configuration
|
||||
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
|
||||
public class AppConfig {
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
[source,xml,indent=0]
|
||||
|
|
@ -6144,7 +6143,7 @@ exact same way as when using Spring annotations:
|
|||
@Configuration
|
||||
@ComponentScan(basePackages = "org.example")
|
||||
public class AppConfig {
|
||||
...
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -6374,7 +6373,7 @@ To enable component scanning, just annotate your `@Configuration` class as follo
|
|||
@Configuration
|
||||
@ComponentScan(basePackages = "com.acme")
|
||||
public class AppConfig {
|
||||
...
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -6693,7 +6692,7 @@ method directly during construction:
|
|||
public Foo foo() {
|
||||
Foo foo = new Foo();
|
||||
foo.init();
|
||||
return foo;
|
||||
return foo;
|
||||
}
|
||||
|
||||
// ...
|
||||
|
|
@ -7011,7 +7010,7 @@ another configuration class:
|
|||
@Configuration
|
||||
public class ConfigA {
|
||||
|
||||
@Bean
|
||||
@Bean
|
||||
public A a() {
|
||||
return new A();
|
||||
}
|
||||
|
|
@ -8681,18 +8680,18 @@ environment provides:
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
public class EntityCreatedEvent<T>
|
||||
extends ApplicationEvent implements ResolvableTypeProvider {
|
||||
extends ApplicationEvent implements ResolvableTypeProvider {
|
||||
|
||||
public EntityCreatedEvent(T entity) {
|
||||
super(entity);
|
||||
}
|
||||
public EntityCreatedEvent(T entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getResolvableType() {
|
||||
return ResolvableType.forClassWithGenerics(getClass(),
|
||||
ResolvableType.forInstance(getSource()));
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public ResolvableType getResolvableType() {
|
||||
return ResolvableType.forClassWithGenerics(getClass(),
|
||||
ResolvableType.forInstance(getSource()));
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[TIP]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
[[databuffers]]
|
||||
= Data Buffers and Codecs
|
||||
|
||||
|
||||
|
||||
|
||||
== Introduction
|
||||
|
||||
The `DataBuffer` interface defines an abstraction over byte buffers.
|
||||
|
|
@ -9,6 +12,9 @@ Netty does not use `ByteBuffer`, but instead offers `ByteBuf` as an alternative.
|
|||
Spring's `DataBuffer` is a simple abstraction over `ByteBuf` that can also be used on non-Netty
|
||||
platforms (i.e. Servlet 3.1+).
|
||||
|
||||
|
||||
|
||||
|
||||
== `DataBufferFactory`
|
||||
|
||||
The `DataBufferFactory` offers functionality to allocate new data buffers, as well as to wrap
|
||||
|
|
@ -25,6 +31,9 @@ to be used on Netty platforms, such as Reactor Netty.
|
|||
The other implementation, the `DefaultDataBufferFactory`, is used on other platforms, such as
|
||||
Servlet 3.1+ servers.
|
||||
|
||||
|
||||
|
||||
|
||||
== The `DataBuffer` interface
|
||||
|
||||
The `DataBuffer` interface is similar to `ByteBuffer`, but offers a number of advantages.
|
||||
|
|
@ -35,7 +44,7 @@ writing, and a separate `flip()` operation to switch between the two I/O operat
|
|||
In general, the following invariant holds for the read position, write position, and the capacity:
|
||||
|
||||
--
|
||||
`0` <= _read position_ <= _write position_ <= _capacity_
|
||||
`0` <= _read position_ <= _write position_ <= _capacity_
|
||||
--
|
||||
|
||||
When reading bytes from the `DataBuffer`, the read position is automatically updated in accordance with
|
||||
|
|
@ -54,6 +63,8 @@ Netty platforms, such as Reactor Netty.
|
|||
The other implementation, the `DefaultDataBuffer`, is used on other platforms, such as Servlet 3.1+
|
||||
servers.
|
||||
|
||||
|
||||
|
||||
=== `PooledDataBuffer`
|
||||
|
||||
The `PooledDataBuffer` is an extension to `DataBuffer` that adds methods for reference counting.
|
||||
|
|
@ -67,6 +78,7 @@ buffers.
|
|||
These methods take a plain `DataBuffer` as parameter, but only call `retain` or `release` if the
|
||||
passed data buffer is an instance of `PooledDataBuffer`.
|
||||
|
||||
|
||||
[[databuffer-reference-counting]]
|
||||
==== Reference Counting
|
||||
|
||||
|
|
@ -95,23 +107,25 @@ throw exceptions:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
DataBufferFactory factory = ...
|
||||
DataBuffer buffer = factory.allocateBuffer(); <1>
|
||||
boolean release = true; <2>
|
||||
try {
|
||||
writeDataToBuffer(buffer); <3>
|
||||
putBufferInHttpBody(buffer);
|
||||
release = false; <4>
|
||||
} finally {
|
||||
if (release) {
|
||||
DataBufferUtils.release(buffer); <5>
|
||||
}
|
||||
}
|
||||
DataBufferFactory factory = ...
|
||||
DataBuffer buffer = factory.allocateBuffer(); <1>
|
||||
boolean release = true; <2>
|
||||
try {
|
||||
writeDataToBuffer(buffer); <3>
|
||||
putBufferInHttpBody(buffer);
|
||||
release = false; <4>
|
||||
}
|
||||
finally {
|
||||
if (release) {
|
||||
DataBufferUtils.release(buffer); <5>
|
||||
}
|
||||
}
|
||||
|
||||
private void writeDataToBuffer(DataBuffer buffer) throws IOException { <3>
|
||||
...
|
||||
}
|
||||
private void writeDataToBuffer(DataBuffer buffer) throws IOException { <3>
|
||||
...
|
||||
}
|
||||
----
|
||||
|
||||
<1> A new buffer is allocated.
|
||||
<2> A boolean flag indicates whether the allocated buffer should be released.
|
||||
<3> This example method loads data into the buffer. Note that the method can throw an `IOException`,
|
||||
|
|
@ -121,6 +135,8 @@ released as part of sending the HTTP body across the wire.
|
|||
<5> If an exception did occur, the flag is still set to `true`, and the buffer will be released
|
||||
here.
|
||||
|
||||
|
||||
|
||||
=== DataBufferUtils
|
||||
|
||||
`DataBufferUtils` contains various utility methods that operate on data buffers.
|
||||
|
|
@ -129,6 +145,9 @@ It contains methods for reading a `Flux` of `DataBuffer` objects from an `InputS
|
|||
`DataBufferUtils` also exposes `retain` and `release` methods that operate on plain `DataBuffer`
|
||||
instances (so that casting to a `PooledDataBuffer` is not required).
|
||||
|
||||
|
||||
|
||||
|
||||
[codecs]
|
||||
== Codecs
|
||||
|
||||
|
|
|
|||
|
|
@ -444,8 +444,8 @@ this:
|
|||
----
|
||||
com/
|
||||
foo/
|
||||
services.xml
|
||||
daos.xml
|
||||
services.xml
|
||||
daos.xml
|
||||
MessengerService.class
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework
|
||||
:toc: left
|
||||
:toclevels: 4
|
||||
:tabsize: 4
|
||||
:docinfo1:
|
||||
|
||||
This part of the reference documentation is concerned with data access and the
|
||||
|
|
@ -1943,13 +1944,13 @@ transaction in which it has been published as committed successfully:
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Component
|
||||
public class MyComponent {
|
||||
public class MyComponent {
|
||||
|
||||
@TransactionalEventListener
|
||||
public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
|
||||
...
|
||||
}
|
||||
}
|
||||
@TransactionalEventListener
|
||||
public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
|
||||
...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The `TransactionalEventListener` annotation exposes a `phase` attribute that allows to customize
|
||||
|
|
@ -4612,13 +4613,13 @@ standalone environment or in a standalone integration test like in the following
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
|
||||
.generateUniqueName(true)
|
||||
.setType(H2)
|
||||
.setScriptEncoding("UTF-8")
|
||||
.ignoreFailedDrops(true)
|
||||
.addScript("schema.sql")
|
||||
.addScripts("user_data.sql", "country_data.sql")
|
||||
.build();
|
||||
.generateUniqueName(true)
|
||||
.setType(H2)
|
||||
.setScriptEncoding("UTF-8")
|
||||
.ignoreFailedDrops(true)
|
||||
.addScript("schema.sql")
|
||||
.addScripts("user_data.sql", "country_data.sql")
|
||||
.build();
|
||||
|
||||
// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)
|
||||
|
||||
|
|
@ -4637,17 +4638,17 @@ Config like in the following example.
|
|||
@Configuration
|
||||
public class DataSourceConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
.generateUniqueName(true)
|
||||
.setType(H2)
|
||||
.setScriptEncoding("UTF-8")
|
||||
.ignoreFailedDrops(true)
|
||||
.addScript("schema.sql")
|
||||
.addScripts("user_data.sql", "country_data.sql")
|
||||
.build();
|
||||
}
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
.generateUniqueName(true)
|
||||
.setType(H2)
|
||||
.setScriptEncoding("UTF-8")
|
||||
.ignoreFailedDrops(true)
|
||||
.addScript("schema.sql")
|
||||
.addScripts("user_data.sql", "country_data.sql")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
:doc-spring-gemfire: {doc-root}/spring-gemfire/docs/current/reference
|
||||
:toc: left
|
||||
:toclevels: 4
|
||||
:tabsize: 4
|
||||
:docinfo1:
|
||||
|
||||
This part of the reference documentation covers the Spring Framework's integration with
|
||||
|
|
@ -1102,8 +1103,8 @@ construct a `HttpComponentsClientHttpRequestFactory` like so:
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
HttpClient httpClient = HttpClientBuilder.create().build();
|
||||
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
RestTemplate restTemplate = new RestTemplate(requestFactory);
|
||||
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
RestTemplate restTemplate = new RestTemplate(requestFactory);
|
||||
----
|
||||
====
|
||||
|
||||
|
|
@ -2447,12 +2448,12 @@ bean as a JMS listener endpoint.
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Component
|
||||
public class MyService {
|
||||
@Component
|
||||
public class MyService {
|
||||
|
||||
@JmsListener(destination = "myDestination")
|
||||
public void processOrder(String data) { ... }
|
||||
}
|
||||
@JmsListener(destination = "myDestination")
|
||||
public void processOrder(String data) { ... }
|
||||
}
|
||||
----
|
||||
|
||||
The idea of the example above is that whenever a message is available on the
|
||||
|
|
@ -2517,12 +2518,12 @@ element.
|
|||
----
|
||||
<jms:annotation-driven/>
|
||||
|
||||
<bean id="jmsListenerContainerFactory"
|
||||
class="org.springframework.jms.config.DefaultJmsListenerContainerFactory">
|
||||
<property name="connectionFactory" ref="connectionFactory"/>
|
||||
<property name="destinationResolver" ref="destinationResolver"/>
|
||||
<property name="concurrency" value="3-10"/>
|
||||
</bean>
|
||||
<bean id="jmsListenerContainerFactory"
|
||||
class="org.springframework.jms.config.DefaultJmsListenerContainerFactory">
|
||||
<property name="connectionFactory" ref="connectionFactory"/>
|
||||
<property name="destinationResolver" ref="destinationResolver"/>
|
||||
<property name="concurrency" value="3-10"/>
|
||||
</bean>
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -2574,10 +2575,10 @@ a custom header:
|
|||
@Component
|
||||
public class MyService {
|
||||
|
||||
@JmsListener(destination = "myDestination")
|
||||
public void processOrder(Order order, @Header("order_type") String orderType) {
|
||||
...
|
||||
}
|
||||
@JmsListener(destination = "myDestination")
|
||||
public void processOrder(Order order, @Header("order_type") String orderType) {
|
||||
...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -2624,17 +2625,17 @@ annotate the payload with `@Valid` and configure the necessary validator as foll
|
|||
@EnableJms
|
||||
public class AppConfig implements JmsListenerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
|
||||
registrar.setMessageHandlerMethodFactory(myJmsHandlerMethodFactory());
|
||||
}
|
||||
@Override
|
||||
public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
|
||||
registrar.setMessageHandlerMethodFactory(myJmsHandlerMethodFactory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
|
||||
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
|
||||
factory.setValidator(myValidator());
|
||||
return factory;
|
||||
}
|
||||
@Bean
|
||||
public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
|
||||
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
|
||||
factory.setValidator(myValidator());
|
||||
return factory;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -2658,8 +2659,8 @@ as follow to automatically send a response:
|
|||
@JmsListener(destination = "myDestination")
|
||||
@SendTo("status")
|
||||
public OrderStatus processOrder(Order order) {
|
||||
// order processing
|
||||
return status;
|
||||
// order processing
|
||||
return status;
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -2678,11 +2679,11 @@ If you need to set additional headers in a transport-independent manner, you cou
|
|||
@JmsListener(destination = "myDestination")
|
||||
@SendTo("status")
|
||||
public Message<OrderStatus> processOrder(Order order) {
|
||||
// order processing
|
||||
return MessageBuilder
|
||||
.withPayload(status)
|
||||
.setHeader("code", 1234)
|
||||
.build();
|
||||
// order processing
|
||||
return MessageBuilder
|
||||
.withPayload(status)
|
||||
.setHeader("code", 1234)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -2695,11 +2696,11 @@ example can be rewritten as follows:
|
|||
----
|
||||
@JmsListener(destination = "myDestination")
|
||||
public JmsResponse<Message<OrderStatus>> processOrder(Order order) {
|
||||
// order processing
|
||||
Message<OrderStatus> response = MessageBuilder
|
||||
.withPayload(status)
|
||||
.setHeader("code", 1234)
|
||||
.build();
|
||||
// order processing
|
||||
Message<OrderStatus> response = MessageBuilder
|
||||
.withPayload(status)
|
||||
.setHeader("code", 1234)
|
||||
.build();
|
||||
return JmsResponse.forQueue(response, "status");
|
||||
}
|
||||
----
|
||||
|
|
@ -3722,7 +3723,7 @@ this interface as the definition for the management interface:
|
|||
<bean id="testBean" class="org.springframework.jmx.JmxTestBean">
|
||||
<property name="name" value="TEST"/>
|
||||
<property name="age" value="100"/>
|
||||
</bean>
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
----
|
||||
|
|
@ -6470,10 +6471,10 @@ however, the exception is uncaught and cannot be transmitted. For those cases, a
|
|||
----
|
||||
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
|
||||
|
||||
@Override
|
||||
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
|
||||
@Override
|
||||
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
|
||||
// handle exception
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -7723,7 +7724,7 @@ the method again.
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@CacheResult(cacheName="books", **exceptionCacheName="failures"**
|
||||
**cachedExceptions = InvalidIsbnNotFoundException.class**)
|
||||
**cachedExceptions = InvalidIsbnNotFoundException.class**)
|
||||
public Book findBook(ISBN isbn)
|
||||
----
|
||||
|
||||
|
|
@ -7853,11 +7854,11 @@ Again, to use it, one simply needs to declare the appropriate `CacheManager`:
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="cacheManager"
|
||||
class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>
|
||||
class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/>
|
||||
|
||||
<!-- EhCache library setup -->
|
||||
<bean id="ehcache"
|
||||
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>
|
||||
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>
|
||||
----
|
||||
|
||||
This setup bootstraps the ehcache library inside Spring IoC (through the `ehcache` bean) which
|
||||
|
|
@ -7878,7 +7879,7 @@ Configuring a `CacheManager` that creates the cache on demand is straightforward
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="cacheManager"
|
||||
class="org.springframework.cache.caffeine.CaffeineCacheManager"/>
|
||||
class="org.springframework.cache.caffeine.CaffeineCacheManager"/>
|
||||
----
|
||||
|
||||
It is also possible to provide the caches to use explicitly. In that case, only those
|
||||
|
|
@ -7924,8 +7925,8 @@ Again, to use it, one simply needs to declare the appropriate `CacheManager`:
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="cacheManager"
|
||||
class="org.springframework.cache.jcache.JCacheCacheManager"
|
||||
p:cache-manager-ref="jCacheManager"/>
|
||||
class="org.springframework.cache.jcache.JCacheCacheManager"
|
||||
p:cache-manager-ref="jCacheManager"/>
|
||||
|
||||
<!-- JSR-107 cache manager setup -->
|
||||
<bean id="jCacheManager" .../>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
[[lanugages]]
|
||||
[[languages]]
|
||||
= Language Support
|
||||
:doc-root: https://docs.spring.io
|
||||
:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework
|
||||
:toc: left
|
||||
:toclevels: 4
|
||||
:tabsize: 4
|
||||
:docinfo1:
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,18 +58,18 @@ for their APIs, thus giving a better Kotlin development experience overall.
|
|||
|
||||
To retrieve a list of `Foo` objects in Java, one would normally write:
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
Flux<User> users = client.get().retrieve().bodyToFlux(User.class)
|
||||
Flux<User> users = client.get().retrieve().bodyToFlux(User.class)
|
||||
----
|
||||
|
||||
Whilst with Kotlin and Spring Framework extensions, one is able to write:
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
val users = client.get().retrieve().bodyToFlux<User>()
|
||||
// or (both are equivalent)
|
||||
val users : Flux<User> = client.get().retrieve().bodyToFlux()
|
||||
val users = client.get().retrieve().bodyToFlux<User>()
|
||||
// or (both are equivalent)
|
||||
val users : Flux<User> = client.get().retrieve().bodyToFlux()
|
||||
----
|
||||
|
||||
As in Java, `users` in Kotlin is strongly typed, but Kotlin's clever type inference allows
|
||||
|
|
@ -172,24 +172,23 @@ This mechanism is very efficient as it does not require any reflection or CGLIB
|
|||
|
||||
In Java, one may for example write:
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.registerBean(Foo.class);
|
||||
context.registerBean(Bar.class, () -> new
|
||||
Bar(context.getBean(Foo.class))
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.registerBean(Foo.class);
|
||||
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class))
|
||||
);
|
||||
----
|
||||
|
||||
Whilst in Kotlin with reified type parameters and `GenericApplicationContext`
|
||||
Kotlin extensions one can instead simply write:
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
val context = GenericApplicationContext().apply {
|
||||
registerBean<Foo>()
|
||||
registerBean { Bar(it.getBean<Foo>()) }
|
||||
}
|
||||
val context = GenericApplicationContext().apply {
|
||||
registerBean<Foo>()
|
||||
registerBean { Bar(it.getBean<Foo>()) }
|
||||
}
|
||||
----
|
||||
|
||||
In order to allow a more declarative approach and cleaner syntax, Spring Framework provides
|
||||
|
|
@ -198,36 +197,36 @@ It declares an `ApplicationContextInitializer` via a clean declarative API
|
|||
which enables one to deal with profiles and `Environment` for customizing
|
||||
how beans are registered.
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
fun beans() = beans {
|
||||
bean<UserHandler>()
|
||||
bean<Routes>()
|
||||
bean<WebHandler>("webHandler") {
|
||||
RouterFunctions.toWebHandler(
|
||||
ref<Routes>().router(),
|
||||
HandlerStrategies.builder().viewResolver(ref()).build()
|
||||
)
|
||||
}
|
||||
bean("messageSource") {
|
||||
ReloadableResourceBundleMessageSource().apply {
|
||||
setBasename("messages")
|
||||
setDefaultEncoding("UTF-8")
|
||||
}
|
||||
}
|
||||
bean {
|
||||
val prefix = "classpath:/templates/"
|
||||
val suffix = ".mustache"
|
||||
val loader = MustacheResourceTemplateLoader(prefix, suffix)
|
||||
MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply {
|
||||
setPrefix(prefix)
|
||||
setSuffix(suffix)
|
||||
}
|
||||
}
|
||||
profile("foo") {
|
||||
bean<Foo>()
|
||||
}
|
||||
}
|
||||
fun beans() = beans {
|
||||
bean<UserHandler>()
|
||||
bean<Routes>()
|
||||
bean<WebHandler>("webHandler") {
|
||||
RouterFunctions.toWebHandler(
|
||||
ref<Routes>().router(),
|
||||
HandlerStrategies.builder().viewResolver(ref()).build()
|
||||
)
|
||||
}
|
||||
bean("messageSource") {
|
||||
ReloadableResourceBundleMessageSource().apply {
|
||||
setBasename("messages")
|
||||
setDefaultEncoding("UTF-8")
|
||||
}
|
||||
}
|
||||
bean {
|
||||
val prefix = "classpath:/templates/"
|
||||
val suffix = ".mustache"
|
||||
val loader = MustacheResourceTemplateLoader(prefix, suffix)
|
||||
MustacheViewResolver(Mustache.compiler().withLoader(loader)).apply {
|
||||
setPrefix(prefix)
|
||||
setSuffix(suffix)
|
||||
}
|
||||
}
|
||||
profile("foo") {
|
||||
bean<Foo>()
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In this example, `bean<Routes>()` is using autowiring by constructor and `ref<Routes>()`
|
||||
|
|
@ -235,12 +234,12 @@ is a shortcut for `applicationContext.getBean(Routes::class.java)`.
|
|||
|
||||
This `beans()` function can then be used to register beans on the application context.
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
val context = GenericApplicationContext().apply {
|
||||
beans().initialize(this)
|
||||
refresh()
|
||||
}
|
||||
val context = GenericApplicationContext().apply {
|
||||
beans().initialize(this)
|
||||
refresh()
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
|
|
@ -276,24 +275,24 @@ Spring Framework now comes with a
|
|||
that allows one to leverage the <<web-reactive#webflux-fn,WebFlux functional
|
||||
API>> for writing clean and idiomatic Kotlin code:
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
router {
|
||||
accept(TEXT_HTML).nest {
|
||||
GET("/") { ok().render("index") }
|
||||
GET("/sse") { ok().render("sse") }
|
||||
GET("/users", userHandler::findAllView)
|
||||
}
|
||||
"/api".nest {
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/users", userHandler::findAll)
|
||||
}
|
||||
accept(TEXT_EVENT_STREAM).nest {
|
||||
GET("/users", userHandler::stream)
|
||||
}
|
||||
}
|
||||
resources("/**", ClassPathResource("static/"))
|
||||
}
|
||||
router {
|
||||
accept(TEXT_HTML).nest {
|
||||
GET("/") { ok().render("index") }
|
||||
GET("/sse") { ok().render("sse") }
|
||||
GET("/users", userHandler::findAllView)
|
||||
}
|
||||
"/api".nest {
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/users", userHandler::findAll)
|
||||
}
|
||||
accept(TEXT_EVENT_STREAM).nest {
|
||||
GET("/users", userHandler::stream)
|
||||
}
|
||||
}
|
||||
resources("/**", ClassPathResource("static/"))
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
|
|
@ -326,18 +325,18 @@ https://github.com/Kotlin/kotlinx.html[kotlinx.html] DSL or simply using Kotlin
|
|||
This can allow one to write Kotlin templates with full autocompletion and
|
||||
refactoring support in a supported IDE:
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
import io.spring.demo.*
|
||||
import io.spring.demo.*
|
||||
|
||||
"""
|
||||
${include("header")}
|
||||
<h1>${i18n("title")}</h1>
|
||||
<ul>
|
||||
${users.joinToLine{ "<li>${i18n("user")} ${it.firstname} ${it.lastname}</li>" }}
|
||||
</ul>
|
||||
${include("footer")}
|
||||
"""
|
||||
"""
|
||||
${include("header")}
|
||||
<h1>${i18n("title")}</h1>
|
||||
<ul>
|
||||
${users.joinToLine{ "<li>${i18n("user")} ${it.firstname} ${it.lastname}</li>" }}
|
||||
</ul>
|
||||
${include("footer")}
|
||||
"""
|
||||
----
|
||||
|
||||
See https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] example
|
||||
|
|
@ -394,9 +393,9 @@ you will be able to write your Kotlin beans without any additional `open` keywor
|
|||
In Kotlin, it is very convenient and considered best practice to declare read-only properties
|
||||
within the primary constructor, as in the following example:
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
class Person(val name: String, val age: Int)
|
||||
class Person(val name: String, val age: Int)
|
||||
----
|
||||
|
||||
You can optionally add https://kotlinlang.org/docs/reference/data-classes.html[the `data` keyword]
|
||||
|
|
@ -410,12 +409,12 @@ in the primary constructor:
|
|||
|
||||
This allows for easy changes to individual properties even if `Person` properties are read-only:
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
data class Person(val name: String, val age: Int)
|
||||
data class Person(val name: String, val age: Int)
|
||||
|
||||
val jack = Person(name = "Jack", age = 1)
|
||||
val olderJack = jack.copy(age = 2)
|
||||
val jack = Person(name = "Jack", age = 1)
|
||||
val olderJack = jack.copy(age = 2)
|
||||
----
|
||||
|
||||
Common persistence technologies such as JPA require a default constructor, preventing this
|
||||
|
|
@ -442,13 +441,13 @@ mappings (like with MongoDB, Redis, Cassandra, etc).
|
|||
Our recommendation is to try and favor constructor injection with `val` read-only (and non-nullable when possible)
|
||||
https://kotlinlang.org/docs/reference/properties.html[properties].
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@Component
|
||||
class YourBean(
|
||||
private val mongoTemplate: MongoTemplate,
|
||||
private val solrClient: SolrClient
|
||||
)
|
||||
@Component
|
||||
class YourBean(
|
||||
private val mongoTemplate: MongoTemplate,
|
||||
private val solrClient: SolrClient
|
||||
)
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
|
|
@ -461,17 +460,17 @@ explicit `@Autowired constructor` in the example shown above.
|
|||
If one really needs to use field injection, use the `lateinit var` construct,
|
||||
i.e.,
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@Component
|
||||
class YourBean {
|
||||
@Component
|
||||
class YourBean {
|
||||
|
||||
@Autowired
|
||||
lateinit var mongoTemplate: MongoTemplate
|
||||
@Autowired
|
||||
lateinit var mongoTemplate: MongoTemplate
|
||||
|
||||
@Autowired
|
||||
lateinit var solrClient: SolrClient
|
||||
}
|
||||
@Autowired
|
||||
lateinit var solrClient: SolrClient
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -487,28 +486,27 @@ character will need to be escaped by writing `@Value("\${property}")`.
|
|||
As an alternative, it is possible to customize the properties placeholder prefix by declaring
|
||||
the following configuration beans:
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@Bean
|
||||
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
|
||||
setPlaceholderPrefix("%{")
|
||||
}
|
||||
@Bean
|
||||
fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
|
||||
setPlaceholderPrefix("%{")
|
||||
}
|
||||
----
|
||||
|
||||
Existing code (like Spring Boot actuators or `@LocalServerPort`) that
|
||||
uses the `${...}` syntax, can be customised with configuration beans, like
|
||||
this:
|
||||
Existing code (like Spring Boot actuators or `@LocalServerPort`) that uses the `${...}` syntax,
|
||||
can be customised with configuration beans, like as follows:
|
||||
|
||||
[source,kotlin]
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@Bean
|
||||
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
|
||||
setPlaceholderPrefix("%{")
|
||||
setIgnoreUnresolvablePlaceholders(true)
|
||||
}
|
||||
@Bean
|
||||
fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply {
|
||||
setPlaceholderPrefix("%{")
|
||||
setIgnoreUnresolvablePlaceholders(true)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
|
||||
@Bean
|
||||
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
|
|
@ -535,20 +533,20 @@ attribute it is specified as a `vararg` parameter.
|
|||
To understand what that means, let's take `@RequestMapping`, which is one
|
||||
of the most widely used Spring annotations as an example. This Java annotation is declared as:
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
public @interface RequestMapping {
|
||||
public @interface RequestMapping {
|
||||
|
||||
@AliasFor("path")
|
||||
String[] value() default {};
|
||||
@AliasFor("path")
|
||||
String[] value() default {};
|
||||
|
||||
@AliasFor("value")
|
||||
String[] path() default {};
|
||||
@AliasFor("value")
|
||||
String[] path() default {};
|
||||
|
||||
RequestMethod[] method() default {};
|
||||
RequestMethod[] method() default {};
|
||||
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
The typical use case for `@RequestMapping` is to map a handler method to a specific path
|
||||
|
|
@ -568,8 +566,8 @@ use a shortcut annotation such as `@GetMapping` or `@PostMapping`, etc.
|
|||
|
||||
[NOTE]
|
||||
====
|
||||
Remininder: if the `@RequestMapping` `method` attribute is not specified, all HTTP methods will be matched,
|
||||
not only the `GET` methods.
|
||||
Reminder: If the `@RequestMapping` `method` attribute is not specified,
|
||||
all HTTP methods will be matched, not only the `GET` methods.
|
||||
====
|
||||
|
||||
Improving the syntax and consistency of Kotlin annotation array attributes is discussed in
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
[[overview]]
|
||||
= Spring Framework Overview
|
||||
:toc: left
|
||||
:toclevels: 1
|
||||
:docinfo1:
|
||||
|
||||
[[overview]]
|
||||
= Spring Framework Overview
|
||||
|
||||
Spring makes it easy to create Java enterprise applications. It provides everything you
|
||||
need to embrace the Java language in an enterprise environment, with support for Groovy
|
||||
and Kotlin as alternative languages on the JVM, and with the flexibility to create many
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ Use this server setup to test one `@Controller` at a time:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
client = WebTestClient.bindToController(new TestController()).build();
|
||||
client = WebTestClient.bindToController(new TestController()).build();
|
||||
----
|
||||
|
||||
The above loads the <<web-reactive.adoc#webflux-config,WebFlux Java config>> and
|
||||
|
|
@ -51,8 +51,8 @@ Use this option to set up a server from a
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
RouterFunction<?> route = ...
|
||||
client = WebTestClient.bindToRouterFunction(route).build();
|
||||
RouterFunction<?> route = ...
|
||||
client = WebTestClient.bindToRouterFunction(route).build();
|
||||
----
|
||||
|
||||
Internally the provided configuration is passed to `RouterFunctions.toWebHandler`.
|
||||
|
|
@ -70,21 +70,20 @@ some subset of it:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration(classes = WebConfig.class) // <1>
|
||||
public class MyTests {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext context; // <2>
|
||||
|
||||
private WebTestClient client;
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration(classes = WebConfig.class) // <1>
|
||||
public class MyTests {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
client = WebTestClient.bindToApplicationContext(context).build(); // <3>
|
||||
}
|
||||
@Autowired
|
||||
private ApplicationContext context; // <2>
|
||||
|
||||
}
|
||||
private WebTestClient client;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
client = WebTestClient.bindToApplicationContext(context).build(); // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
<1> Specify the configuration to load
|
||||
|
|
@ -107,7 +106,7 @@ This server setup option allows you to connect to a running server:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
|
||||
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -122,10 +121,10 @@ are readily available following `bindToServer`. For all others, you need to use
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
client = WebTestClient.bindToController(new TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.build();
|
||||
client = WebTestClient.bindToController(new TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.build();
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -143,12 +142,12 @@ Typically you start by asserting the response status and headers:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
// ...
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON_UTF8)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
|
||||
// ...
|
||||
----
|
||||
|
||||
Then you specify how to decode and consume the response body:
|
||||
|
|
@ -162,32 +161,32 @@ Then you can use built-in assertions for the body. Here is one example:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBodyList(Person.class).hasSize(3).contains(person);
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBodyList(Person.class).hasSize(3).contains(person);
|
||||
----
|
||||
|
||||
You can go beyond the built-in assertions and create your own:
|
||||
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.consumeWith(result -> {
|
||||
// custom assertions (e.g. AssertJ)...
|
||||
});
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.consumeWith(result -> {
|
||||
// custom assertions (e.g. AssertJ)...
|
||||
});
|
||||
----
|
||||
|
||||
You can also exit the workflow and get a result:
|
||||
|
||||
----
|
||||
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
----
|
||||
|
||||
[TIP]
|
||||
|
|
@ -208,10 +207,10 @@ that resources are released:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound()
|
||||
.expectBody(Void.class);
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound()
|
||||
.expectBody(Void.class);
|
||||
----
|
||||
|
||||
Or if you want to assert there is no response content, use this:
|
||||
|
|
@ -219,11 +218,11 @@ Or if you want to assert there is no response content, use this:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
client.post().uri("/persons")
|
||||
.body(personMono, Person.class)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody().isEmpty;
|
||||
client.post().uri("/persons")
|
||||
.body(personMono, Person.class)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody().isEmpty;
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -238,11 +237,11 @@ http://jsonassert.skyscreamer.org[JSONAssert] to verify JSON content:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.json("{\"name\":\"Jane\"}")
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.json("{\"name\":\"Jane\"}")
|
||||
----
|
||||
|
||||
You can also use https://github.com/jayway/JsonPath[JSONPath] expressions:
|
||||
|
|
@ -250,12 +249,12 @@ You can also use https://github.com/jayway/JsonPath[JSONPath] expressions:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$[0].name").isEqualTo("Jane")
|
||||
.jsonPath("$[1].name").isEqualTo("Jason");
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$[0].name").isEqualTo("Jane")
|
||||
.jsonPath("$[1].name").isEqualTo("Jason");
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -269,11 +268,11 @@ and header assertions, as shown below:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
|
||||
.accept(TEXT_EVENT_STREAM)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.returnResult(MyEvent.class);
|
||||
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
|
||||
.accept(TEXT_EVENT_STREAM)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.returnResult(MyEvent.class);
|
||||
|
||||
----
|
||||
|
||||
|
|
@ -284,14 +283,14 @@ from the `reactor-test` module to do that, for example:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
Flux<Event> eventFux = result.getResponseBody();
|
||||
Flux<Event> eventFux = result.getResponseBody();
|
||||
|
||||
StepVerifier.create(eventFlux)
|
||||
.expectNext(person)
|
||||
.expectNextCount(4)
|
||||
.consumeNextWith(p -> ...)
|
||||
.thenCancel()
|
||||
.verify();
|
||||
StepVerifier.create(eventFlux)
|
||||
.expectNext(person)
|
||||
.expectNextCount(4)
|
||||
.consumeNextWith(p -> ...)
|
||||
.thenCancel()
|
||||
.verify();
|
||||
----
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
:doc-spring-boot: {doc-root}/spring-boot/docs/current/reference
|
||||
:toc: left
|
||||
:toclevels: 4
|
||||
:tabsize: 4
|
||||
:docinfo1:
|
||||
|
||||
The adoption of the test-driven-development (TDD) approach to software
|
||||
|
|
@ -1142,13 +1143,13 @@ example, a custom `@EnabledOnMac` annotation can be created as follows.
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@EnabledIf(
|
||||
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
|
||||
reason = "Enabled on Mac OS"
|
||||
)
|
||||
public @interface EnabledOnMac {}
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@EnabledIf(
|
||||
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
|
||||
reason = "Enabled on Mac OS"
|
||||
)
|
||||
public @interface EnabledOnMac {}
|
||||
----
|
||||
|
||||
===== @DisabledIf
|
||||
|
|
@ -1179,13 +1180,13 @@ example, a custom `@DisabledOnMac` annotation can be created as follows.
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@DisabledIf(
|
||||
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
|
||||
reason = "Disabled on Mac OS"
|
||||
)
|
||||
public @interface DisabledOnMac {}
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@DisabledIf(
|
||||
expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
|
||||
reason = "Disabled on Mac OS"
|
||||
)
|
||||
public @interface DisabledOnMac {}
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -3901,15 +3902,15 @@ empty list in order to disable the default listeners, which otherwise would requ
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner.class)
|
||||
@TestExecutionListeners({})
|
||||
public class SimpleTest {
|
||||
@RunWith(SpringRunner.class)
|
||||
@TestExecutionListeners({})
|
||||
public class SimpleTest {
|
||||
|
||||
@Test
|
||||
public void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[testcontext-junit4-rules]]
|
||||
|
|
@ -3938,21 +3939,21 @@ demonstrates the proper way to declare these rules in an integration test.
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
// Optionally specify a non-Spring Runner via @RunWith(...)
|
||||
@ContextConfiguration
|
||||
public class IntegrationTest {
|
||||
// Optionally specify a non-Spring Runner via @RunWith(...)
|
||||
@ContextConfiguration
|
||||
public class IntegrationTest {
|
||||
|
||||
@ClassRule
|
||||
public static final SpringClassRule springClassRule = new SpringClassRule();
|
||||
@ClassRule
|
||||
public static final SpringClassRule springClassRule = new SpringClassRule();
|
||||
|
||||
@Rule
|
||||
public final SpringMethodRule springMethodRule = new SpringMethodRule();
|
||||
@Rule
|
||||
public final SpringMethodRule springMethodRule = new SpringMethodRule();
|
||||
|
||||
@Test
|
||||
public void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[testcontext-support-classes-junit4]]
|
||||
|
|
@ -4026,17 +4027,17 @@ The following code listing demonstrates how to configure a test class to use the
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
// Instructs JUnit Jupiter to extend the test with Spring support.
|
||||
@ExtendWith(SpringExtension.class)
|
||||
// Instructs Spring to load an ApplicationContext from TestConfig.class
|
||||
@ContextConfiguration(classes = TestConfig.class)
|
||||
class SimpleTests {
|
||||
// Instructs JUnit Jupiter to extend the test with Spring support.
|
||||
@ExtendWith(SpringExtension.class)
|
||||
// Instructs Spring to load an ApplicationContext from TestConfig.class
|
||||
@ContextConfiguration(classes = TestConfig.class)
|
||||
class SimpleTests {
|
||||
|
||||
@Test
|
||||
void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
@Test
|
||||
void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Since annotations in JUnit 5 can also be used as meta-annotations, Spring is able to
|
||||
|
|
@ -4049,16 +4050,16 @@ configuration used in the previous example.
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
// Instructs Spring to register the SpringExtension with JUnit
|
||||
// Jupiter and load an ApplicationContext from TestConfig.class
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class SimpleTests {
|
||||
// Instructs Spring to register the SpringExtension with JUnit
|
||||
// Jupiter and load an ApplicationContext from TestConfig.class
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class SimpleTests {
|
||||
|
||||
@Test
|
||||
void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
@Test
|
||||
void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Similarly, the following example uses `@SpringJUnitWebConfig` to create a
|
||||
|
|
@ -4067,16 +4068,16 @@ Similarly, the following example uses `@SpringJUnitWebConfig` to create a
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
// Instructs Spring to register the SpringExtension with JUnit
|
||||
// Jupiter and load a WebApplicationContext from TestWebConfig.class
|
||||
@SpringJUnitWebConfig(TestWebConfig.class)
|
||||
class SimpleWebTests {
|
||||
// Instructs Spring to register the SpringExtension with JUnit
|
||||
// Jupiter and load a WebApplicationContext from TestWebConfig.class
|
||||
@SpringJUnitWebConfig(TestWebConfig.class)
|
||||
class SimpleWebTests {
|
||||
|
||||
@Test
|
||||
void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
@Test
|
||||
void testMethod() {
|
||||
// execute test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in
|
||||
|
|
@ -4121,18 +4122,18 @@ dependencies to be `final` and therefore _immutable_.
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class OrderServiceIntegrationTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class OrderServiceIntegrationTests {
|
||||
|
||||
private final OrderService orderService;
|
||||
private final OrderService orderService;
|
||||
|
||||
@Autowired
|
||||
OrderServiceIntegrationTests(OrderService orderService) {
|
||||
this.orderService = orderService.
|
||||
}
|
||||
@Autowired
|
||||
OrderServiceIntegrationTests(OrderService orderService) {
|
||||
this.orderService = orderService.
|
||||
}
|
||||
|
||||
// tests that use the injected OrderService
|
||||
}
|
||||
// tests that use the injected OrderService
|
||||
}
|
||||
----
|
||||
|
||||
[[testcontext-junit-jupiter-di-method]]
|
||||
|
|
@ -4149,14 +4150,14 @@ In the following example, Spring will inject the `OrderService` from the
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class OrderServiceIntegrationTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class OrderServiceIntegrationTests {
|
||||
|
||||
@Test
|
||||
void deleteOrder(@Autowired OrderService orderService) {
|
||||
// use orderService from the test's ApplicationContext
|
||||
}
|
||||
}
|
||||
@Test
|
||||
void deleteOrder(@Autowired OrderService orderService) {
|
||||
// use orderService from the test's ApplicationContext
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Due to the robustness of the `ParameterResolver` support in JUnit Jupiter, it is also
|
||||
|
|
@ -4171,17 +4172,17 @@ use of `@RepeatedTest` from JUnit Jupiter allows the test method to gain access
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class OrderServiceIntegrationTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class OrderServiceIntegrationTests {
|
||||
|
||||
@RepeatedTest(10)
|
||||
void placeOrderRepeatedly(RepetitionInfo repetitionInfo,
|
||||
@Autowired OrderService orderService) {
|
||||
@RepeatedTest(10)
|
||||
void placeOrderRepeatedly(RepetitionInfo repetitionInfo,
|
||||
@Autowired OrderService orderService) {
|
||||
|
||||
// use orderService from the test's ApplicationContext
|
||||
// and repetitionInfo from JUnit Jupiter
|
||||
}
|
||||
}
|
||||
// use orderService from the test's ApplicationContext
|
||||
// and repetitionInfo from JUnit Jupiter
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[testcontext-support-classes-testng]]
|
||||
|
|
@ -4276,29 +4277,28 @@ JUnit Jupiter based example of using Spring MVC Test:
|
|||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
|
||||
class ExampleTests {
|
||||
@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
|
||||
class ExampleTests {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setup(WebApplicationContext wac) {
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
|
||||
}
|
||||
@BeforeEach
|
||||
void setup(WebApplicationContext wac) {
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAccount() throws Exception {
|
||||
this.mockMvc.perform(get("/accounts/1")
|
||||
.accept(MediaType.parseMediaType("application/json;charset=UTF-8")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType("application/json"))
|
||||
.andExpect(jsonPath("$.name").value("Lee"));
|
||||
}
|
||||
|
||||
}
|
||||
@Test
|
||||
void getAccount() throws Exception {
|
||||
this.mockMvc.perform(get("/accounts/1")
|
||||
.accept(MediaType.parseMediaType("application/json;charset=UTF-8")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentType("application/json"))
|
||||
.andExpect(jsonPath("$.name").value("Lee"));
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The above test relies on the `WebApplicationContext` support of the __TestContext framework__
|
||||
|
|
@ -4461,13 +4461,13 @@ session across requests. It can be used as follows:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
// static import of SharedHttpSessionConfigurer.sharedHttpSession
|
||||
// static import of SharedHttpSessionConfigurer.sharedHttpSession
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
|
||||
.apply(sharedHttpSession())
|
||||
.build();
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
|
||||
.apply(sharedHttpSession())
|
||||
.build();
|
||||
|
||||
// Use mockMvc to perform requests...
|
||||
// Use mockMvc to perform requests...
|
||||
----
|
||||
|
||||
See `ConfigurableMockMvcBuilder` for a list of all MockMvc builder features
|
||||
|
|
@ -4761,45 +4761,45 @@ paging through all messages. How would you go about testing it?
|
|||
|
||||
With Spring MVC Test, we can easily test if we are able to create a `Message`.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
MockHttpServletRequestBuilder createMessage = post("/messages/")
|
||||
.param("summary", "Spring Rocks")
|
||||
.param("text", "In case you didn't know, Spring Rocks!");
|
||||
MockHttpServletRequestBuilder createMessage = post("/messages/")
|
||||
.param("summary", "Spring Rocks")
|
||||
.param("text", "In case you didn't know, Spring Rocks!");
|
||||
|
||||
mockMvc.perform(createMessage)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/messages/123"));
|
||||
mockMvc.perform(createMessage)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/messages/123"));
|
||||
----
|
||||
|
||||
What if we want to test our form view that allows us to create the message? For example,
|
||||
assume our form looks like the following snippet:
|
||||
|
||||
[source,xml]
|
||||
[source,xml,indent=0]
|
||||
----
|
||||
<form id="messageForm" action="/messages/" method="post">
|
||||
<div class="pull-right"><a href="/messages/">Messages</a></div>
|
||||
<form id="messageForm" action="/messages/" method="post">
|
||||
<div class="pull-right"><a href="/messages/">Messages</a></div>
|
||||
|
||||
<label for="summary">Summary</label>
|
||||
<input type="text" class="required" id="summary" name="summary" value="" />
|
||||
<label for="summary">Summary</label>
|
||||
<input type="text" class="required" id="summary" name="summary" value="" />
|
||||
|
||||
<label for="text">Message</label>
|
||||
<textarea id="text" name="text"></textarea>
|
||||
<label for="text">Message</label>
|
||||
<textarea id="text" name="text"></textarea>
|
||||
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="Create" />
|
||||
</div>
|
||||
</form>
|
||||
<div class="form-actions">
|
||||
<input type="submit" value="Create" />
|
||||
</div>
|
||||
</form>
|
||||
----
|
||||
|
||||
How do we ensure that our form will produce the correct request to create a new message? A
|
||||
naive attempt would look like this:
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
mockMvc.perform(get("/messages/form"))
|
||||
.andExpect(xpath("//input[@name='summary']").exists())
|
||||
.andExpect(xpath("//textarea[@name='text']").exists());
|
||||
mockMvc.perform(get("/messages/form"))
|
||||
.andExpect(xpath("//input[@name='summary']").exists())
|
||||
.andExpect(xpath("//textarea[@name='text']").exists());
|
||||
----
|
||||
|
||||
This test has some obvious drawbacks. If we update our controller to use the parameter
|
||||
|
|
@ -4807,21 +4807,21 @@ This test has some obvious drawbacks. If we update our controller to use the par
|
|||
form is out of synch with the controller. To resolve this we can combine our two tests.
|
||||
|
||||
[[spring-mvc-test-server-htmlunit-mock-mvc-test]]
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
String summaryParamName = "summary";
|
||||
String textParamName = "text";
|
||||
mockMvc.perform(get("/messages/form"))
|
||||
.andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
|
||||
.andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());
|
||||
String summaryParamName = "summary";
|
||||
String textParamName = "text";
|
||||
mockMvc.perform(get("/messages/form"))
|
||||
.andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
|
||||
.andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());
|
||||
|
||||
MockHttpServletRequestBuilder createMessage = post("/messages/")
|
||||
.param(summaryParamName, "Spring Rocks")
|
||||
.param(textParamName, "In case you didn't know, Spring Rocks!");
|
||||
MockHttpServletRequestBuilder createMessage = post("/messages/")
|
||||
.param(summaryParamName, "Spring Rocks")
|
||||
.param(textParamName, "In case you didn't know, Spring Rocks!");
|
||||
|
||||
mockMvc.perform(createMessage)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/messages/123"));
|
||||
mockMvc.perform(createMessage)
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/messages/123"));
|
||||
----
|
||||
|
||||
This would reduce the risk of our test incorrectly passing, but there are still some
|
||||
|
|
@ -4908,19 +4908,19 @@ In order to use HtmlUnit with Apache HttpComponents 4.5+, you will need to use H
|
|||
We can easily create an HtmlUnit `WebClient` that integrates with `MockMvc` using the
|
||||
`MockMvcWebClientBuilder` as follows.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@Autowired
|
||||
WebApplicationContext context;
|
||||
@Autowired
|
||||
WebApplicationContext context;
|
||||
|
||||
WebClient webClient;
|
||||
WebClient webClient;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
@Before
|
||||
public void setup() {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
|
|
@ -4941,9 +4941,9 @@ Now we can use HtmlUnit as we normally would, but without the need to deploy our
|
|||
application to a Servlet container. For example, we can request the view to create
|
||||
a message with the following.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
|
||||
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
|
|
@ -4955,29 +4955,29 @@ illustrated in <<Advanced MockMvcWebClientBuilder>>.
|
|||
Once we have a reference to the `HtmlPage`, we can then fill out the form and submit
|
||||
it to create a message.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
|
||||
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
|
||||
summaryInput.setValueAttribute("Spring Rocks");
|
||||
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
|
||||
textInput.setText("In case you didn't know, Spring Rocks!");
|
||||
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
|
||||
HtmlPage newMessagePage = submit.click();
|
||||
HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
|
||||
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
|
||||
summaryInput.setValueAttribute("Spring Rocks");
|
||||
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
|
||||
textInput.setText("In case you didn't know, Spring Rocks!");
|
||||
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
|
||||
HtmlPage newMessagePage = submit.click();
|
||||
----
|
||||
|
||||
Finally, we can verify that a new message was created successfully. The following
|
||||
assertions use the http://joel-costigliola.github.io/assertj/[AssertJ] library.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
|
||||
String id = newMessagePage.getHtmlElementById("id").getTextContent();
|
||||
assertThat(id).isEqualTo("123");
|
||||
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
|
||||
assertThat(summary).isEqualTo("Spring Rocks");
|
||||
String text = newMessagePage.getHtmlElementById("text").getTextContent();
|
||||
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
|
||||
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
|
||||
String id = newMessagePage.getHtmlElementById("id").getTextContent();
|
||||
assertThat(id).isEqualTo("123");
|
||||
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
|
||||
assertThat(summary).isEqualTo("Spring Rocks");
|
||||
String text = newMessagePage.getHtmlElementById("text").getTextContent();
|
||||
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
|
||||
----
|
||||
|
||||
This improves on our <<spring-mvc-test-server-htmlunit-mock-mvc-test,MockMvc test>> in a
|
||||
|
|
@ -4999,59 +4999,59 @@ In the examples so far, we have used `MockMvcWebClientBuilder` in the simplest w
|
|||
by building a `WebClient` based on the `WebApplicationContext` loaded for us by the Spring
|
||||
TestContext Framework. This approach is repeated here.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@Autowired
|
||||
WebApplicationContext context;
|
||||
@Autowired
|
||||
WebApplicationContext context;
|
||||
|
||||
WebClient webClient;
|
||||
WebClient webClient;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
@Before
|
||||
public void setup() {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
We can also specify additional configuration options.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
WebClient webClient;
|
||||
WebClient webClient;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
// demonstrates applying a MockMvcConfigurer (Spring Security)
|
||||
.webAppContextSetup(context, springSecurity())
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com","example.org")
|
||||
.build();
|
||||
}
|
||||
@Before
|
||||
public void setup() {
|
||||
webClient = MockMvcWebClientBuilder
|
||||
// demonstrates applying a MockMvcConfigurer (Spring Security)
|
||||
.webAppContextSetup(context, springSecurity())
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com","example.org")
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
As an alternative, we can perform the exact same setup by configuring the `MockMvc`
|
||||
instance separately and supplying it to the `MockMvcWebClientBuilder` as follows.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
MockMvc mockMvc = MockMvcBuilders
|
||||
.webAppContextSetup(context)
|
||||
.apply(springSecurity())
|
||||
.build();
|
||||
MockMvc mockMvc = MockMvcBuilders
|
||||
.webAppContextSetup(context)
|
||||
.apply(springSecurity())
|
||||
.build();
|
||||
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.mockMvcSetup(mockMvc)
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com","example.org")
|
||||
.build();
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.mockMvcSetup(mockMvc)
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com","example.org")
|
||||
.build();
|
||||
----
|
||||
|
||||
This is more verbose, but by building the `WebClient` with a `MockMvc` instance we have
|
||||
|
|
@ -5094,27 +5094,27 @@ be displayed afterwards.
|
|||
If one of the fields were named "summary", then we might have something like the
|
||||
following repeated in multiple places within our tests.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
|
||||
summaryInput.setValueAttribute(summary);
|
||||
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
|
||||
summaryInput.setValueAttribute(summary);
|
||||
----
|
||||
|
||||
So what happens if we change the `id` to "smmry"? Doing so would force us to update all
|
||||
of our tests to incorporate this change! Of course, this violates the _DRY Principle_; so
|
||||
we should ideally extract this code into its own method as follows.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
|
||||
setSummary(currentPage, summary);
|
||||
// ...
|
||||
}
|
||||
public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) {
|
||||
setSummary(currentPage, summary);
|
||||
// ...
|
||||
}
|
||||
|
||||
public void setSummary(HtmlPage currentPage, String summary) {
|
||||
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
|
||||
summaryInput.setValueAttribute(summary);
|
||||
}
|
||||
public void setSummary(HtmlPage currentPage, String summary) {
|
||||
HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
|
||||
summaryInput.setValueAttribute(summary);
|
||||
}
|
||||
----
|
||||
|
||||
This ensures that we do not have to update all of our tests if we change the UI.
|
||||
|
|
@ -5122,39 +5122,39 @@ This ensures that we do not have to update all of our tests if we change the UI.
|
|||
We might even take this a step further and place this logic within an Object that
|
||||
represents the `HtmlPage` we are currently on.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
public class CreateMessagePage {
|
||||
public class CreateMessagePage {
|
||||
|
||||
final HtmlPage currentPage;
|
||||
final HtmlPage currentPage;
|
||||
|
||||
final HtmlTextInput summaryInput;
|
||||
final HtmlTextInput summaryInput;
|
||||
|
||||
final HtmlSubmitInput submit;
|
||||
final HtmlSubmitInput submit;
|
||||
|
||||
public CreateMessagePage(HtmlPage currentPage) {
|
||||
this.currentPage = currentPage;
|
||||
this.summaryInput = currentPage.getHtmlElementById("summary");
|
||||
this.submit = currentPage.getHtmlElementById("submit");
|
||||
public CreateMessagePage(HtmlPage currentPage) {
|
||||
this.currentPage = currentPage;
|
||||
this.summaryInput = currentPage.getHtmlElementById("summary");
|
||||
this.submit = currentPage.getHtmlElementById("submit");
|
||||
}
|
||||
|
||||
public <T> T createMessage(String summary, String text) throws Exception {
|
||||
setSummary(summary);
|
||||
|
||||
HtmlPage result = submit.click();
|
||||
boolean error = CreateMessagePage.at(result);
|
||||
|
||||
return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
|
||||
}
|
||||
|
||||
public void setSummary(String summary) throws Exception {
|
||||
summaryInput.setValueAttribute(summary);
|
||||
}
|
||||
|
||||
public static boolean at(HtmlPage page) {
|
||||
return "Create Message".equals(page.getTitleText());
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T createMessage(String summary, String text) throws Exception {
|
||||
setSummary(summary);
|
||||
|
||||
HtmlPage result = submit.click();
|
||||
boolean error = CreateMessagePage.at(result);
|
||||
|
||||
return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
|
||||
}
|
||||
|
||||
public void setSummary(String summary) throws Exception {
|
||||
summaryInput.setValueAttribute(summary);
|
||||
}
|
||||
|
||||
public static boolean at(HtmlPage page) {
|
||||
return "Create Message".equals(page.getTitleText());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Formerly, this pattern is known as the
|
||||
|
|
@ -5171,18 +5171,18 @@ includes a test dependency on `org.seleniumhq.selenium:selenium-htmlunit-driver`
|
|||
We can easily create a Selenium `WebDriver` that integrates with `MockMvc` using the
|
||||
`MockMvcHtmlUnitDriverBuilder` as follows.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@Autowired
|
||||
WebApplicationContext context;
|
||||
@Autowired
|
||||
WebApplicationContext context;
|
||||
|
||||
WebDriver driver;
|
||||
WebDriver driver;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
@Before
|
||||
public void setup() {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -5204,17 +5204,17 @@ Now we can use WebDriver as we normally would, but without the need to deploy ou
|
|||
application to a Servlet container. For example, we can request the view to create
|
||||
a message with the following.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
CreateMessagePage page = CreateMessagePage.to(driver);
|
||||
CreateMessagePage page = CreateMessagePage.to(driver);
|
||||
----
|
||||
|
||||
We can then fill out the form and submit it to create a message.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
ViewMessagePage viewMessagePage =
|
||||
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
|
||||
ViewMessagePage viewMessagePage =
|
||||
page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
|
||||
----
|
||||
|
||||
This improves on the design of our
|
||||
|
|
@ -5223,35 +5223,35 @@ Pattern_. As we mentioned in <<spring-mvc-test-server-htmlunit-webdriver-why>>,
|
|||
use the Page Object Pattern with HtmlUnit, but it is much easier with WebDriver. Let's
|
||||
take a look at our new `CreateMessagePage` implementation.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
public class CreateMessagePage
|
||||
extends AbstractPage { // <1>
|
||||
public class CreateMessagePage
|
||||
extends AbstractPage { // <1>
|
||||
|
||||
// <2>
|
||||
private WebElement summary;
|
||||
private WebElement text;
|
||||
// <2>
|
||||
private WebElement summary;
|
||||
private WebElement text;
|
||||
|
||||
// <3>
|
||||
@FindBy(css = "input[type=submit]")
|
||||
private WebElement submit;
|
||||
// <3>
|
||||
@FindBy(css = "input[type=submit]")
|
||||
private WebElement submit;
|
||||
|
||||
public CreateMessagePage(WebDriver driver) {
|
||||
super(driver);
|
||||
public CreateMessagePage(WebDriver driver) {
|
||||
super(driver);
|
||||
}
|
||||
|
||||
public <T> T createMessage(Class<T> resultPage, String summary, String details) {
|
||||
this.summary.sendKeys(summary);
|
||||
this.text.sendKeys(details);
|
||||
this.submit.click();
|
||||
return PageFactory.initElements(driver, resultPage);
|
||||
}
|
||||
|
||||
public static CreateMessagePage to(WebDriver driver) {
|
||||
driver.get("http://localhost:9990/mail/messages/form");
|
||||
return PageFactory.initElements(driver, CreateMessagePage.class);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T createMessage(Class<T> resultPage, String summary, String details) {
|
||||
this.summary.sendKeys(summary);
|
||||
this.text.sendKeys(details);
|
||||
this.submit.click();
|
||||
return PageFactory.initElements(driver, resultPage);
|
||||
}
|
||||
|
||||
public static CreateMessagePage to(WebDriver driver) {
|
||||
driver.get("http://localhost:9990/mail/messages/form");
|
||||
return PageFactory.initElements(driver, CreateMessagePage.class);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
<1> The first thing you will notice is that `CreateMessagePage` extends the
|
||||
|
|
@ -5277,39 +5277,39 @@ annotation to look up our submit button using a css selector, *input[type=submit
|
|||
Finally, we can verify that a new message was created successfully. The following
|
||||
assertions use the https://code.google.com/p/fest/[FEST assertion library].
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
|
||||
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
|
||||
assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
|
||||
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
|
||||
----
|
||||
|
||||
We can see that our `ViewMessagePage` allows us to interact with our custom domain
|
||||
model. For example, it exposes a method that returns a `Message` object.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
public Message getMessage() throws ParseException {
|
||||
Message message = new Message();
|
||||
message.setId(getId());
|
||||
message.setCreated(getCreated());
|
||||
message.setSummary(getSummary());
|
||||
message.setText(getText());
|
||||
return message;
|
||||
}
|
||||
public Message getMessage() throws ParseException {
|
||||
Message message = new Message();
|
||||
message.setId(getId());
|
||||
message.setCreated(getCreated());
|
||||
message.setSummary(getSummary());
|
||||
message.setText(getText());
|
||||
return message;
|
||||
}
|
||||
----
|
||||
|
||||
We can then leverage the rich domain objects in our assertions.
|
||||
|
||||
Lastly, don't forget to _close_ the `WebDriver` instance when the test is complete.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@After
|
||||
public void destroy() {
|
||||
if (driver != null) {
|
||||
driver.close();
|
||||
@After
|
||||
public void destroy() {
|
||||
if (driver != null) {
|
||||
driver.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
For additional information on using WebDriver, refer to the Selenium
|
||||
|
|
@ -5322,59 +5322,59 @@ In the examples so far, we have used `MockMvcHtmlUnitDriverBuilder` in the simpl
|
|||
possible, by building a `WebDriver` based on the `WebApplicationContext` loaded for us by
|
||||
the Spring TestContext Framework. This approach is repeated here.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@Autowired
|
||||
WebApplicationContext context;
|
||||
@Autowired
|
||||
WebApplicationContext context;
|
||||
|
||||
WebDriver driver;
|
||||
WebDriver driver;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
@Before
|
||||
public void setup() {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.webAppContextSetup(context)
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
We can also specify additional configuration options.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
WebDriver driver;
|
||||
WebDriver driver;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
// demonstrates applying a MockMvcConfigurer (Spring Security)
|
||||
.webAppContextSetup(context, springSecurity())
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com","example.org")
|
||||
.build();
|
||||
@Before
|
||||
public void setup() {
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
// demonstrates applying a MockMvcConfigurer (Spring Security)
|
||||
.webAppContextSetup(context, springSecurity())
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com","example.org")
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
As an alternative, we can perform the exact same setup by configuring the `MockMvc`
|
||||
instance separately and supplying it to the `MockMvcHtmlUnitDriverBuilder` as follows.
|
||||
|
||||
[source,java]
|
||||
[source,java,indent=0]
|
||||
----
|
||||
MockMvc mockMvc = MockMvcBuilders
|
||||
.webAppContextSetup(context)
|
||||
.apply(springSecurity())
|
||||
.build();
|
||||
MockMvc mockMvc = MockMvcBuilders
|
||||
.webAppContextSetup(context)
|
||||
.apply(springSecurity())
|
||||
.build();
|
||||
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.mockMvcSetup(mockMvc)
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com","example.org")
|
||||
.build();
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.mockMvcSetup(mockMvc)
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com","example.org")
|
||||
.build();
|
||||
----
|
||||
|
||||
This is more verbose, but by building the `WebDriver` with a `MockMvc` instance we have
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework
|
||||
:toc: left
|
||||
:toclevels: 4
|
||||
:tabsize: 4
|
||||
:docinfo1:
|
||||
|
||||
This part of the documentation covers support for reactive stack, web applications built on a
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
:api-spring-framework: {doc-root}/spring-framework/docs/{spring-version}/javadoc-api/org/springframework
|
||||
:toc: left
|
||||
:toclevels: 4
|
||||
:tabsize: 4
|
||||
:docinfo1:
|
||||
|
||||
This part of the documentation covers support for Servlet stack, web applications built on the
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ The `retrieve()` method is the easiest way to get a response body and decode it:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
WebClient client = WebClient.create("http://example.org");
|
||||
WebClient client = WebClient.create("http://example.org");
|
||||
|
||||
Mono<Person> result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(Person.class);
|
||||
Mono<Person> result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(Person.class);
|
||||
----
|
||||
|
||||
You can also get a stream of objects decoded from the response:
|
||||
|
|
@ -45,10 +45,10 @@ You can also get a stream of objects decoded from the response:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
Flux<Quote> result = client.get()
|
||||
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.retrieve()
|
||||
.bodyToFlux(Quote.class);
|
||||
Flux<Quote> result = client.get()
|
||||
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.retrieve()
|
||||
.bodyToFlux(Quote.class);
|
||||
----
|
||||
|
||||
By default, responses with 4xx or 5xx status codes result in an error of type
|
||||
|
|
@ -57,12 +57,12 @@ By default, responses with 4xx or 5xx status codes result in an error of type
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
Mono<Person> result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatus::is4xxServerError, response -> ...)
|
||||
.onStatus(HttpStatus::is5xxServerError, response -> ...)
|
||||
.bodyToMono(Person.class);
|
||||
Mono<Person> result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatus::is4xxServerError, response -> ...)
|
||||
.onStatus(HttpStatus::is5xxServerError, response -> ...)
|
||||
.bodyToMono(Person.class);
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -77,10 +77,10 @@ to `retrieve()` but also provides access to the `ClientResponse`:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
Mono<Person> result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.flatMap(response -> response.bodyToMono(Person.class));
|
||||
Mono<Person> result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.flatMap(response -> response.bodyToMono(Person.class));
|
||||
----
|
||||
|
||||
At this level you can also create a full `ResponseEntity`:
|
||||
|
|
@ -88,10 +88,10 @@ At this level you can also create a full `ResponseEntity`:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
Mono<ResponseEntity<Person>> result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.flatMap(response -> response.toEntity(Person.class));
|
||||
Mono<ResponseEntity<Person>> result = client.get()
|
||||
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.flatMap(response -> response.toEntity(Person.class));
|
||||
----
|
||||
|
||||
Note that unlike `retrieve()`, with `exchange()` there are no automatic error signals for
|
||||
|
|
@ -117,14 +117,14 @@ The request body can be encoded from an Object:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
Mono<Person> personMono = ... ;
|
||||
Mono<Person> personMono = ... ;
|
||||
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/persons/{id}", id)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(personMono, Person.class)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/persons/{id}", id)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(personMono, Person.class)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
----
|
||||
|
||||
You can also have a stream of objects encoded:
|
||||
|
|
@ -132,14 +132,14 @@ You can also have a stream of objects encoded:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
Flux<Person> personFlux = ... ;
|
||||
Flux<Person> personFlux = ... ;
|
||||
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/persons/{id}", id)
|
||||
.contentType(MediaType.APPLICATION_STREAM_JSON)
|
||||
.body(personFlux, Person.class)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/persons/{id}", id)
|
||||
.contentType(MediaType.APPLICATION_STREAM_JSON)
|
||||
.body(personFlux, Person.class)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
----
|
||||
|
||||
Or if you have the actual value, use the `syncBody` shortcut method:
|
||||
|
|
@ -147,14 +147,14 @@ Or if you have the actual value, use the `syncBody` shortcut method:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
Person person = ... ;
|
||||
Person person = ... ;
|
||||
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/persons/{id}", id)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.syncBody(person)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/persons/{id}", id)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.syncBody(person)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -169,13 +169,13 @@ content is automatically set to `"application/x-www-form-urlencoded"` by the
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
MultiValueMap<String, String> formData = ... ;
|
||||
MultiValueMap<String, String> formData = ... ;
|
||||
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/path", id)
|
||||
.syncBody(formData)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/path", id)
|
||||
.syncBody(formData)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
----
|
||||
|
||||
You can also supply form data in-line via `BodyInserters`:
|
||||
|
|
@ -183,13 +183,13 @@ You can also supply form data in-line via `BodyInserters`:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
import static org.springframework.web.reactive.function.BodyInserters.*;
|
||||
import static org.springframework.web.reactive.function.BodyInserters.*;
|
||||
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/path", id)
|
||||
.body(fromFormData("k1", "v1").with("k2", "v2"))
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/path", id)
|
||||
.body(fromFormData("k1", "v1").with("k2", "v2"))
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -204,18 +204,18 @@ headers. `MultipartBodyBuilder` can be used to build the parts:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
MultipartBodyBuilder builder = new MultipartBodyBuilder();
|
||||
builder.part("fieldPart", "fieldValue");
|
||||
builder.part("filePart", new FileSystemResource("...logo.png"));
|
||||
builder.part("jsonPart", new Person("Jason"));
|
||||
MultipartBodyBuilder builder = new MultipartBodyBuilder();
|
||||
builder.part("fieldPart", "fieldValue");
|
||||
builder.part("filePart", new FileSystemResource("...logo.png"));
|
||||
builder.part("jsonPart", new Person("Jason"));
|
||||
|
||||
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
|
||||
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
|
||||
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/path", id)
|
||||
.syncBody(parts)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/path", id)
|
||||
.syncBody(parts)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
----
|
||||
|
||||
Note that the content type for each part is automatically set based on the extension of the
|
||||
|
|
@ -227,13 +227,13 @@ You can also supply multipart data in-line via `BodyInserters`:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
import static org.springframework.web.reactive.function.BodyInserters.*;
|
||||
import static org.springframework.web.reactive.function.BodyInserters.*;
|
||||
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/path", id)
|
||||
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
Mono<Void> result = client.post()
|
||||
.uri("/path", id)
|
||||
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -250,14 +250,14 @@ To customize the underlying HTTP client:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
SslContext sslContext = ...
|
||||
SslContext sslContext = ...
|
||||
|
||||
ClientHttpConnector connector = new ReactorClientHttpConnector(
|
||||
builder -> builder.sslContext(sslContext));
|
||||
ClientHttpConnector connector = new ReactorClientHttpConnector(
|
||||
builder -> builder.sslContext(sslContext));
|
||||
|
||||
WebClient webClient = WebClient.builder()
|
||||
.clientConnector(connector)
|
||||
.build();
|
||||
WebClient webClient = WebClient.builder()
|
||||
.clientConnector(connector)
|
||||
.build();
|
||||
----
|
||||
|
||||
To customize the <<web-reactive.adoc#webflux-codecs,HTTP codecs>> used for encoding and
|
||||
|
|
@ -266,16 +266,15 @@ decoding HTTP messages:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
ExchangeStrategies strategies = ExchangeStrategies.builder()
|
||||
.codecs(configurer -> {
|
||||
// ...
|
||||
})
|
||||
.build();
|
||||
|
||||
WebClient webClient = WebClient.builder()
|
||||
.exchangeStrategies(strategies)
|
||||
.build();
|
||||
ExchangeStrategies strategies = ExchangeStrategies.builder()
|
||||
.codecs(configurer -> {
|
||||
// ...
|
||||
})
|
||||
.build();
|
||||
|
||||
WebClient webClient = WebClient.builder()
|
||||
.exchangeStrategies(strategies)
|
||||
.build();
|
||||
----
|
||||
|
||||
The builder can be used to insert <<webflux-client-filter>>.
|
||||
|
|
@ -289,9 +288,9 @@ build a new `WebClient`, based on, but without affecting the current instance:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
WebClient modifiedClient = client.mutate()
|
||||
// user builder methods...
|
||||
.build();
|
||||
WebClient modifiedClient = client.mutate()
|
||||
// user builder methods...
|
||||
.build();
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -305,16 +304,14 @@ build a new `WebClient`, based on, but without affecting the current instance:
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
WebClient client = WebClient.builder()
|
||||
.filter((request, next) -> {
|
||||
|
||||
ClientRequest filtered = ClientRequest.from(request)
|
||||
.header("foo", "bar")
|
||||
.build();
|
||||
|
||||
return next.exchange(filtered);
|
||||
})
|
||||
.build();
|
||||
WebClient client = WebClient.builder()
|
||||
.filter((request, next) -> {
|
||||
ClientRequest filtered = ClientRequest.from(request)
|
||||
.header("foo", "bar")
|
||||
.build();
|
||||
return next.exchange(filtered);
|
||||
})
|
||||
.build();
|
||||
----
|
||||
|
||||
`ExchangeFilterFunctions` provides a filter for basic authentication:
|
||||
|
|
@ -323,11 +320,11 @@ build a new `WebClient`, based on, but without affecting the current instance:
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
|
||||
// static import of ExchangeFilterFunctions.basicAuthentication
|
||||
// static import of ExchangeFilterFunctions.basicAuthentication
|
||||
|
||||
WebClient client = WebClient.builder()
|
||||
.filter(basicAuthentication("user", "pwd"))
|
||||
.build();
|
||||
WebClient client = WebClient.builder()
|
||||
.filter(basicAuthentication("user", "pwd"))
|
||||
.build();
|
||||
----
|
||||
|
||||
You can also mutate an existing `WebClient` instance without affecting the original:
|
||||
|
|
@ -335,7 +332,7 @@ You can also mutate an existing `WebClient` instance without affecting the origi
|
|||
[source,java,intent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
WebClient filteredClient = client.mutate()
|
||||
.filter(basicAuthentication("user", "pwd")
|
||||
.build();
|
||||
WebClient filteredClient = client.mutate()
|
||||
.filter(basicAuthentication("user", "pwd")
|
||||
.build();
|
||||
----
|
||||
|
|
|
|||
|
|
@ -28,17 +28,16 @@ Creating a WebSocket server is as simple as implementing `WebSocketHandler`:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||
import org.springframework.web.reactive.socket.WebSocketSession;
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||
import org.springframework.web.reactive.socket.WebSocketSession;
|
||||
|
||||
public class MyWebSocketHandler implements WebSocketHandler {
|
||||
public class MyWebSocketHandler implements WebSocketHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
// ...
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Spring WebFlux provides a `WebSocketHandlerAdapter` that can adapt WebSocket
|
||||
|
|
@ -49,26 +48,25 @@ adapter is registered as a bean, you can map requests to your handler, for examp
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
static class WebConfig {
|
||||
@Configuration
|
||||
static class WebConfig {
|
||||
|
||||
@Bean
|
||||
public HandlerMapping handlerMapping() {
|
||||
Map<String, WebSocketHandler> map = new HashMap<>();
|
||||
map.put("/path", new MyWebSocketHandler());
|
||||
@Bean
|
||||
public HandlerMapping handlerMapping() {
|
||||
Map<String, WebSocketHandler> map = new HashMap<>();
|
||||
map.put("/path", new MyWebSocketHandler());
|
||||
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setUrlMap(map);
|
||||
mapping.setOrder(-1); // before annotated controllers
|
||||
return mapping;
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setUrlMap(map);
|
||||
mapping.setOrder(-1); // before annotated controllers
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketHandlerAdapter handlerAdapter() {
|
||||
return new WebSocketHandlerAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketHandlerAdapter handlerAdapter() {
|
||||
return new WebSocketHandlerAdapter();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -98,22 +96,21 @@ WebSocket options when running on Tomcat:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
static class WebConfig {
|
||||
@Configuration
|
||||
static class WebConfig {
|
||||
|
||||
@Bean
|
||||
public WebSocketHandlerAdapter handlerAdapter() {
|
||||
return new WebSocketHandlerAdapter(webSocketService());
|
||||
}
|
||||
@Bean
|
||||
public WebSocketHandlerAdapter handlerAdapter() {
|
||||
return new WebSocketHandlerAdapter(webSocketService());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketService webSocketService() {
|
||||
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
|
||||
strategy.setMaxSessionIdleTimeout(0L);
|
||||
return new HandshakeWebSocketService(strategy);
|
||||
}
|
||||
|
||||
}
|
||||
@Bean
|
||||
public WebSocketService webSocketService() {
|
||||
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
|
||||
strategy.setMaxSessionIdleTimeout(0L);
|
||||
return new HandshakeWebSocketService(strategy);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Check the upgrade strategy for your server to see what options are available. Currently
|
||||
|
|
@ -157,9 +154,9 @@ WebSocketClient client = new ReactorNettyWebSocketClient();
|
|||
|
||||
URI url = new URI("ws://localhost:8080/path");
|
||||
client.execute(url, session ->
|
||||
session.receive()
|
||||
.doOnNext(System.out::println)
|
||||
.then());
|
||||
session.receive()
|
||||
.doOnNext(System.out::println)
|
||||
.then());
|
||||
----
|
||||
|
||||
Some clients, e.g. Jetty, implement `Lifecycle` and need to be started in stopped
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ Configuring the Groovy Markup Template Engine is quite easy:
|
|||
@EnableWebMvc
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.groovy();
|
||||
}
|
||||
|
|
@ -1287,7 +1287,7 @@ The HTML would look like:
|
|||
<input type="submit" value="Save Changes"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
</form>
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -47,26 +47,23 @@ the `DispatcherServlet`. This class is auto-detected by the Servlet container
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
public class MyWebApplicationInitializer implements WebApplicationInitializer {
|
||||
public class MyWebApplicationInitializer implements WebApplicationInitializer {
|
||||
|
||||
@Override
|
||||
public void onStartup(ServletContext servletCxt) {
|
||||
@Override
|
||||
public void onStartup(ServletContext servletCxt) {
|
||||
|
||||
// Load Spring web application configuration
|
||||
AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext();
|
||||
cxt.register(AppConfig.class);
|
||||
cxt.refresh();
|
||||
// Load Spring web application configuration
|
||||
AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
|
||||
ac.register(AppConfig.class);
|
||||
ac.refresh();
|
||||
|
||||
// Create DispatcherServlet
|
||||
DispatcherServlet servlet = new DispatcherServlet(cxt);
|
||||
|
||||
// Register and map the Servlet
|
||||
ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
|
||||
registration.setLoadOnStartup(1);
|
||||
registration.addMapping("/app/*");
|
||||
}
|
||||
|
||||
}
|
||||
// Create and register the DispatcherServlet
|
||||
DispatcherServlet servlet = new DispatcherServlet(ac);
|
||||
ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
|
||||
registration.setLoadOnStartup(1);
|
||||
registration.addMapping("/app/*");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
|
|
@ -83,29 +80,29 @@ Below is an example of `web.xml` configuration to register and initialize the `D
|
|||
----
|
||||
<web-app>
|
||||
|
||||
<listener>
|
||||
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
|
||||
</listener>
|
||||
<listener>
|
||||
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<context-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>/WEB-INF/app-context.xml</param-value>
|
||||
</context-param>
|
||||
<context-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>/WEB-INF/app-context.xml</param-value>
|
||||
</context-param>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>app</servlet-name>
|
||||
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
|
||||
<init-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value></param-value>
|
||||
</init-param>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
<servlet>
|
||||
<servlet-name>app</servlet-name>
|
||||
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
|
||||
<init-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value></param-value>
|
||||
</init-param>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>app</servlet-name>
|
||||
<url-pattern>/app/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
<servlet-mapping>
|
||||
<servlet-name>app</servlet-name>
|
||||
<url-pattern>/app/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
</web-app>
|
||||
----
|
||||
|
|
@ -150,23 +147,23 @@ Below is example configuration with a `WebApplicationContext` hierarchy:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
|
||||
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getRootConfigClasses() {
|
||||
return new Class[] { RootConfig.class };
|
||||
}
|
||||
@Override
|
||||
protected Class<?>[] getRootConfigClasses() {
|
||||
return new Class<?[] { RootConfig.class };
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getServletConfigClasses() {
|
||||
return new Class[] { App1Config.class };
|
||||
}
|
||||
@Override
|
||||
protected Class<?>[] getServletConfigClasses() {
|
||||
return new Class<?[] { App1Config.class };
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getServletMappings() {
|
||||
return new String[] { "/app1/*" };
|
||||
}
|
||||
}
|
||||
@Override
|
||||
protected String[] getServletMappings() {
|
||||
return new String[] { "/app1/*" };
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[TIP]
|
||||
|
|
@ -182,29 +179,29 @@ And the `web.xml` equivalent:
|
|||
----
|
||||
<web-app>
|
||||
|
||||
<listener>
|
||||
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
|
||||
</listener>
|
||||
<listener>
|
||||
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
|
||||
</listener>
|
||||
|
||||
<context-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>/WEB-INF/root-context.xml</param-value>
|
||||
</context-param>
|
||||
<context-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>/WEB-INF/root-context.xml</param-value>
|
||||
</context-param>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>app1</servlet-name>
|
||||
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
|
||||
<init-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>/WEB-INF/app1-context.xml</param-value>
|
||||
</init-param>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
<servlet>
|
||||
<servlet-name>app1</servlet-name>
|
||||
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
|
||||
<init-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>/WEB-INF/app1-context.xml</param-value>
|
||||
</init-param>
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>app1</servlet-name>
|
||||
<url-pattern>/app1/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
<servlet-mapping>
|
||||
<servlet-name>app1</servlet-name>
|
||||
<url-pattern>/app1/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
</web-app>
|
||||
----
|
||||
|
|
@ -319,7 +316,6 @@ example of registering a `DispatcherServlet`:
|
|||
registration.setLoadOnStartup(1);
|
||||
registration.addMapping("/");
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -351,7 +347,6 @@ This is recommended for applications that use Java-based Spring configuration:
|
|||
protected String[] getServletMappings() {
|
||||
return new String[] { "/" };
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -379,7 +374,6 @@ If using XML-based Spring configuration, you should extend directly from
|
|||
protected String[] getServletMappings() {
|
||||
return new String[] { "/" };
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -1933,13 +1927,10 @@ following the `@ModelAttribute` argument:
|
|||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(**@ModelAttribute("pet") Pet pet**, BindingResult result) {
|
||||
|
||||
if (result.hasErrors()) {
|
||||
return "petForm";
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -1955,21 +1946,20 @@ controller or alternatively use the `binding` flag on the annotation:
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@ModelAttribute
|
||||
public AccountForm setUpForm() {
|
||||
return new AccountForm();
|
||||
}
|
||||
public AccountForm setUpForm() {
|
||||
return new AccountForm();
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public Account findAccount(@PathVariable String accountId) {
|
||||
return accountRepository.findOne(accountId);
|
||||
}
|
||||
@ModelAttribute
|
||||
public Account findAccount(@PathVariable String accountId) {
|
||||
return accountRepository.findOne(accountId);
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
public String update(@Valid AccountUpdateForm form, BindingResult result,
|
||||
**@ModelAttribute(binding=false)** Account account) {
|
||||
|
||||
// ...
|
||||
}
|
||||
@PostMapping("update")
|
||||
public String update(@Valid AccountUpdateForm form, BindingResult result,
|
||||
**@ModelAttribute(binding=false)** Account account) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
In addition to data binding you can also invoke validation using your own custom
|
||||
|
|
@ -1982,14 +1972,11 @@ subsequently reported back to the user:
|
|||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(**@ModelAttribute("pet") Pet pet**, BindingResult result) {
|
||||
|
||||
new PetValidator().validate(pet, result);
|
||||
if (result.hasErrors()) {
|
||||
return "petForm";
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -2001,13 +1988,10 @@ annotation:
|
|||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(**@Valid @ModelAttribute("pet") Pet pet**, BindingResult result) {
|
||||
|
||||
if (result.hasErrors()) {
|
||||
return "petForm";
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -2327,31 +2311,32 @@ object and converting from an object to the HTTP response body. The
|
|||
`RequestMappingHandlerAdapter` supports the `@RequestBody` annotation with the following
|
||||
default `HttpMessageConverters`:
|
||||
|
||||
* `ByteArrayHttpMessageConverter` converts byte arrays.
|
||||
* `StringHttpMessageConverter` converts strings.
|
||||
* `FormHttpMessageConverter` converts form data to/from a MultiValueMap<String, String>.
|
||||
* `SourceHttpMessageConverter` converts to/from a javax.xml.transform.Source.
|
||||
* `ByteArrayHttpMessageConverter` converts byte arrays
|
||||
* `StringHttpMessageConverter` converts strings
|
||||
* `FormHttpMessageConverter` converts form data to/from a `MultiValueMap<String, String>`
|
||||
* `SourceHttpMessageConverter` converts to/from a `javax.xml.transform.Source``
|
||||
|
||||
For more information on these converters, see <<integration.adoc#rest-message-conversion,
|
||||
Message Converters>>. Also note that if using the MVC namespace or the MVC Java config, a
|
||||
wider range of message converters are registered by default. See <<mvc-config-enable>> for
|
||||
more information.
|
||||
Message Converters>>. Also note that if using the MVC namespace or the MVC Java config,
|
||||
a wider range of message converters are registered by default, including default JSON
|
||||
and XML payload converters (if e.g. Jackson, Gson and/or JAXB2 are present at runtime).
|
||||
See <<mvc-config-enable>> for more information on MVC setup options.
|
||||
|
||||
If you intend to read and write XML, you will need to configure the
|
||||
`MarshallingHttpMessageConverter` with a specific `Marshaller` and an `Unmarshaller`
|
||||
implementation from the `org.springframework.oxm` package. The example below shows how
|
||||
to do that directly in your configuration but if your application is configured through
|
||||
the MVC namespace or the MVC Java config see <<mvc-config-enable>> instead.
|
||||
For a custom example, if you intend to read and write XML using the `spring-oxm` module,
|
||||
you need to configure the `MarshallingHttpMessageConverter` with a specific `Marshaller`
|
||||
implementation from the `org.springframework.oxm` package. The example below shows how to
|
||||
do that directly in your configuration but if your application is configured through the
|
||||
MVC namespace or the MVC Java config see <<mvc-config-enable>> instead.
|
||||
|
||||
[source,xml,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
|
||||
<property name="messageConverters">
|
||||
<util:list id="beanList">
|
||||
<list>
|
||||
<ref bean="stringHttpMessageConverter"/>
|
||||
<ref bean="marshallingHttpMessageConverter"/>
|
||||
</util:list>
|
||||
<list>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
|
|
@ -2360,11 +2345,10 @@ the MVC namespace or the MVC Java config see <<mvc-config-enable>> instead.
|
|||
|
||||
<bean id="marshallingHttpMessageConverter"
|
||||
class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
|
||||
<property name="marshaller" ref="castorMarshaller"/>
|
||||
<property name="unmarshaller" ref="castorMarshaller"/>
|
||||
<constructor-arg ref="xstreamMarshaller"/>
|
||||
</bean>
|
||||
|
||||
<bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>
|
||||
<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/>
|
||||
----
|
||||
|
||||
An `@RequestBody` method parameter can be annotated with `@Valid`, in which case it will
|
||||
|
|
@ -2833,9 +2817,8 @@ Spring MVC also provides a mechanism for building links to controller methods. F
|
|||
|
||||
@GetMapping("/bookings/{booking}")
|
||||
public String getBooking(@PathVariable Long booking) {
|
||||
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -2923,12 +2906,12 @@ For example given:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@RequestMapping("/people/{id}/addresses")
|
||||
public class PersonAddressController {
|
||||
@RequestMapping("/people/{id}/addresses")
|
||||
public class PersonAddressController {
|
||||
|
||||
@RequestMapping("/{country}")
|
||||
public HttpEntity getAddress(@PathVariable String country) { ... }
|
||||
}
|
||||
@RequestMapping("/{country}")
|
||||
public HttpEntity getAddress(@PathVariable String country) { ... }
|
||||
}
|
||||
----
|
||||
|
||||
You can prepare a link from a JSP as follows:
|
||||
|
|
@ -3822,16 +3805,16 @@ accepted as an argument in several Spring Web MVC APIs.
|
|||
[subs="verbatim,quotes"]
|
||||
----
|
||||
// Cache for an hour - "Cache-Control: max-age=3600"
|
||||
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
|
||||
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
|
||||
|
||||
// Prevent caching - "Cache-Control: no-store"
|
||||
CacheControl ccNoStore = CacheControl.noStore();
|
||||
// Prevent caching - "Cache-Control: no-store"
|
||||
CacheControl ccNoStore = CacheControl.noStore();
|
||||
|
||||
// Cache for ten days in public and private caches,
|
||||
// public caches should not transform the response
|
||||
// "Cache-Control: max-age=864000, public, no-transform"
|
||||
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS)
|
||||
.noTransform().cachePublic();
|
||||
// Cache for ten days in public and private caches,
|
||||
// public caches should not transform the response
|
||||
// "Cache-Control: max-age=864000, public, no-transform"
|
||||
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS)
|
||||
.noTransform().cachePublic();
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -4029,7 +4012,7 @@ In XML use the `<mvc:annotation-driven>` element:
|
|||
|
||||
The above registers a number of Spring MVC
|
||||
<<mvc-servlet-special-bean-types,infrastructure beans>> also adapting to dependencies
|
||||
available on the classpath -- for JSON, XML, etc.
|
||||
available on the classpath: e.g. payload converters for JSON, XML, etc.
|
||||
|
||||
|
||||
|
||||
|
|
@ -4047,7 +4030,6 @@ In Java config implement `WebMvcConfigurer` interface:
|
|||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
// Implement configuration methods...
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4079,7 +4061,6 @@ In Java config, register custom formatters and converters:
|
|||
public void addFormatters(FormatterRegistry registry) {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4153,7 +4134,6 @@ In Java config, you can customize the global `Validator` instance:
|
|||
public Validator getValidator(); {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4219,7 +4199,6 @@ In Java config, register interceptors to apply to incoming requests:
|
|||
registry.addInterceptor(new ThemeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
|
||||
registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4324,7 +4303,6 @@ Below is an example that adds Jackson JSON and XML converters with a customized
|
|||
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
|
||||
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.xml().build()));
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4365,23 +4343,23 @@ It is also possible to do the same in XML:
|
|||
[source,xml,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
<mvc:annotation-driven>
|
||||
<mvc:message-converters>
|
||||
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
|
||||
<property name="objectMapper" ref="objectMapper"/>
|
||||
</bean>
|
||||
<bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
|
||||
<property name="objectMapper" ref="xmlMapper"/>
|
||||
</bean>
|
||||
</mvc:message-converters>
|
||||
</mvc:annotation-driven>
|
||||
<mvc:annotation-driven>
|
||||
<mvc:message-converters>
|
||||
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
|
||||
<property name="objectMapper" ref="objectMapper"/>
|
||||
</bean>
|
||||
<bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
|
||||
<property name="objectMapper" ref="xmlMapper"/>
|
||||
</bean>
|
||||
</mvc:message-converters>
|
||||
</mvc:annotation-driven>
|
||||
|
||||
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
|
||||
p:indentOutput="true"
|
||||
p:simpleDateFormat="yyyy-MM-dd"
|
||||
p:modulesToInstall="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>
|
||||
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
|
||||
p:indentOutput="true"
|
||||
p:simpleDateFormat="yyyy-MM-dd"
|
||||
p:modulesToInstall="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>
|
||||
|
||||
<bean id="xmlMapper" parent="objectMapper" p:createXmlMapper="true"/>
|
||||
<bean id="xmlMapper" parent="objectMapper" p:createXmlMapper="true"/>
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -4406,7 +4384,6 @@ An example of forwarding a request for `"/"` to a view called `"home"` in Java:
|
|||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("/").setViewName("home");
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4442,7 +4419,6 @@ JSON rendering:
|
|||
registry.enableContentNegotiation(new MappingJackson2JsonView());
|
||||
registry.jsp();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4506,7 +4482,6 @@ In Java config simply add the respective "Configurer" bean:
|
|||
configurer.setTemplateLoaderPath("/WEB-INF/");
|
||||
return configurer;
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4541,7 +4516,6 @@ In Java config:
|
|||
.addResourceLocations("/public", "classpath:/static/")
|
||||
.setCachePeriod(31556926);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4584,7 +4558,6 @@ For example in Java config;
|
|||
.resourceChain(true)
|
||||
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4647,7 +4620,6 @@ To enable the feature using the default setup use:
|
|||
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
|
||||
configurer.enable();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -4713,21 +4685,21 @@ Example in Java config:
|
|||
@Override
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
configurer
|
||||
.setUseSuffixPatternMatch(true)
|
||||
.setUseTrailingSlashMatch(false)
|
||||
.setUseRegisteredSuffixPatternMatch(true)
|
||||
.setPathMatcher(antPathMatcher())
|
||||
.setUrlPathHelper(urlPathHelper());
|
||||
.setUseSuffixPatternMatch(true)
|
||||
.setUseTrailingSlashMatch(false)
|
||||
.setUseRegisteredSuffixPatternMatch(true)
|
||||
.setPathMatcher(antPathMatcher())
|
||||
.setUrlPathHelper(urlPathHelper());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UrlPathHelper urlPathHelper() {
|
||||
//...
|
||||
//...
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PathMatcher antPathMatcher() {
|
||||
//...
|
||||
//...
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4738,17 +4710,17 @@ In XML, the same:
|
|||
[source,xml,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
<mvc:annotation-driven>
|
||||
<mvc:path-matching
|
||||
suffix-pattern="true"
|
||||
trailing-slash="false"
|
||||
registered-suffixes-only="true"
|
||||
path-helper="pathHelper"
|
||||
path-matcher="pathMatcher"/>
|
||||
</mvc:annotation-driven>
|
||||
<mvc:annotation-driven>
|
||||
<mvc:path-matching
|
||||
suffix-pattern="true"
|
||||
trailing-slash="false"
|
||||
registered-suffixes-only="true"
|
||||
path-helper="pathHelper"
|
||||
path-matcher="pathMatcher"/>
|
||||
</mvc:annotation-driven>
|
||||
|
||||
<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
|
||||
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>
|
||||
<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
|
||||
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -4797,7 +4769,6 @@ hook of the Spring `ApplicationContext`:
|
|||
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -828,20 +828,19 @@ Consider also customizing these server-side SockJS related properties (see Javad
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
|
||||
@Configuration
|
||||
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/sockjs").withSockJS()
|
||||
.setStreamBytesLimit(512 * 1024)
|
||||
.setHttpMessageCacheSize(1000)
|
||||
.setDisconnectDelay(30 * 1000);
|
||||
}
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/sockjs").withSockJS()
|
||||
.setStreamBytesLimit(512 * 1024)
|
||||
.setHttpMessageCacheSize(1000)
|
||||
.setDisconnectDelay(30 * 1000);
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -992,10 +991,10 @@ broadcasting to other connected clients):
|
|||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.setApplicationDestinationPrefixes("/app");
|
||||
config.enableSimpleBroker("/topic", "/queue");
|
||||
}
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.setApplicationDestinationPrefixes("/app");
|
||||
config.enableSimpleBroker("/topic", "/queue");
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
|
@ -1458,20 +1457,19 @@ In Java config:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
|
||||
// ...
|
||||
// ...
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableStompBrokerRelay("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
registry.setPathMatcher(new AntPathMatcher("."));
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableStompBrokerRelay("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
registry.setPathMatcher(new AntPathMatcher("."));
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In XML config:
|
||||
|
|
@ -1479,25 +1477,25 @@ In XML config:
|
|||
[source,xml,indent=0]
|
||||
[subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/websocket
|
||||
http://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/websocket
|
||||
http://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
||||
|
||||
<websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
|
||||
<websocket:stomp-endpoint path="/stomp" />
|
||||
<websocket:simple-broker prefix="/topic, /queue"/>
|
||||
</websocket:message-broker>
|
||||
<websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
|
||||
<websocket:stomp-endpoint path="/stomp"/>
|
||||
<websocket:simple-broker prefix="/topic, /queue"/>
|
||||
</websocket:message-broker>
|
||||
|
||||
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
|
||||
<constructor-arg index="0" value="." />
|
||||
</bean>
|
||||
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
|
||||
<constructor-arg index="0" value="."/>
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
</beans>
|
||||
----
|
||||
|
||||
And below is a simple example to illustrate a controller with "." separator:
|
||||
|
|
@ -1505,15 +1503,15 @@ And below is a simple example to illustrate a controller with "." separator:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Controller
|
||||
@MessageMapping("foo")
|
||||
public class FooController {
|
||||
|
||||
@MessageMapping("bar.{baz}")
|
||||
public void handleBaz(@DestinationVariable String baz) {
|
||||
}
|
||||
@Controller
|
||||
@MessageMapping("foo")
|
||||
public class FooController {
|
||||
|
||||
}
|
||||
@MessageMapping("bar.{baz}")
|
||||
public void handleBaz(@DestinationVariable String baz) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
If the application prefix is set to "/app" then the foo method is effectively mapped to "/app/foo.bar.{baz}".
|
||||
|
|
@ -1612,30 +1610,26 @@ user and associate it with subsequent STOMP messages on the same session:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class MyConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class MyConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
registration.setInterceptors(new ChannelInterceptorAdapter() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
|
||||
StompHeaderAccessor accessor =
|
||||
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
|
||||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
Authentication user = ... ; // access authentication header(s)
|
||||
accessor.setUser(user);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
registration.setInterceptors(new ChannelInterceptorAdapter() {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor =
|
||||
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
Authentication user = ... ; // access authentication header(s)
|
||||
accessor.setUser(user);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Also note that when using Spring Security's authorization for messages, at present
|
||||
|
|
@ -1674,16 +1668,16 @@ the class-level to share a common destination):
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Controller
|
||||
public class PortfolioController {
|
||||
@Controller
|
||||
public class PortfolioController {
|
||||
|
||||
@MessageMapping("/trade")
|
||||
@SendToUser("/queue/position-updates")
|
||||
public TradeResult executeTrade(Trade trade, Principal principal) {
|
||||
// ...
|
||||
return tradeResult;
|
||||
}
|
||||
}
|
||||
@MessageMapping("/trade")
|
||||
@SendToUser("/queue/position-updates")
|
||||
public TradeResult executeTrade(Trade trade, Principal principal) {
|
||||
// ...
|
||||
return tradeResult;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
If the user has more than one session, by default all of the sessions subscribed
|
||||
|
|
@ -1694,24 +1688,23 @@ setting the `broadcast` attribute to false, for example:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Controller
|
||||
public class MyController {
|
||||
@Controller
|
||||
public class MyController {
|
||||
|
||||
@MessageMapping("/action")
|
||||
public void handleAction() throws Exception{
|
||||
// raise MyBusinessException here
|
||||
}
|
||||
@MessageMapping("/action")
|
||||
public void handleAction() throws Exception{
|
||||
// raise MyBusinessException here
|
||||
}
|
||||
|
||||
@MessageExceptionHandler
|
||||
@SendToUser(destinations="/queue/errors", broadcast=false)
|
||||
public ApplicationError handleException(MyBusinessException exception) {
|
||||
// ...
|
||||
return appError;
|
||||
}
|
||||
}
|
||||
@MessageExceptionHandler
|
||||
@SendToUser(destinations="/queue/errors", broadcast=false)
|
||||
public ApplicationError handleException(MyBusinessException exception) {
|
||||
// ...
|
||||
return appError;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
While user destinations generally imply an authenticated user, it isn't required
|
||||
|
|
@ -1817,15 +1810,15 @@ to intercept inbound messages:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
registration.setInterceptors(new MyChannelInterceptor());
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
registration.setInterceptors(new MyChannelInterceptor());
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
A custom `ChannelInterceptor` can extend the empty method base class
|
||||
|
|
@ -1835,16 +1828,16 @@ to access information about the message.
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
public class MyChannelInterceptor extends ChannelInterceptorAdapter {
|
||||
public class MyChannelInterceptor extends ChannelInterceptorAdapter {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
|
||||
StompCommand command = accessor.getStompCommand();
|
||||
// ...
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
|
||||
StompCommand command = accessor.getStompCommand();
|
||||
// ...
|
||||
return message;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
|
@ -1859,10 +1852,10 @@ To begin create and configure `WebSocketStompClient`:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
WebSocketClient webSocketClient = new StandardWebSocketClient();
|
||||
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
|
||||
stompClient.setMessageConverter(new StringMessageConverter());
|
||||
stompClient.setTaskScheduler(taskScheduler); // for heartbeats
|
||||
WebSocketClient webSocketClient = new StandardWebSocketClient();
|
||||
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
|
||||
stompClient.setMessageConverter(new StringMessageConverter());
|
||||
stompClient.setTaskScheduler(taskScheduler); // for heartbeats
|
||||
----
|
||||
|
||||
In the above example `StandardWebSocketClient` could be replaced with `SockJsClient`
|
||||
|
|
@ -1875,9 +1868,9 @@ Next establish a connection and provide a handler for the STOMP session:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
String url = "ws://127.0.0.1:8080/endpoint";
|
||||
StompSessionHandler sessionHandler = new MyStompSessionHandler();
|
||||
stompClient.connect(url, sessionHandler);
|
||||
String url = "ws://127.0.0.1:8080/endpoint";
|
||||
StompSessionHandler sessionHandler = new MyStompSessionHandler();
|
||||
stompClient.connect(url, sessionHandler);
|
||||
----
|
||||
|
||||
When the session is ready for use the handler is notified:
|
||||
|
|
@ -1887,10 +1880,10 @@ When the session is ready for use the handler is notified:
|
|||
----
|
||||
public class MyStompSessionHandler extends StompSessionHandlerAdapter {
|
||||
|
||||
@Override
|
||||
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
|
||||
// ...
|
||||
}
|
||||
@Override
|
||||
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -1913,15 +1906,15 @@ Object type the payload should be deserialized to:
|
|||
----
|
||||
session.subscribe("/topic/foo", new StompFrameHandler() {
|
||||
|
||||
@Override
|
||||
public Type getPayloadType(StompHeaders headers) {
|
||||
return String.class;
|
||||
}
|
||||
@Override
|
||||
public Type getPayloadType(StompHeaders headers) {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFrame(StompHeaders headers, Object payload) {
|
||||
// ...
|
||||
}
|
||||
@Override
|
||||
public void handleFrame(StompHeaders headers, Object payload) {
|
||||
// ...
|
||||
}
|
||||
|
||||
});
|
||||
----
|
||||
|
|
@ -1970,11 +1963,11 @@ inbound client messages and may be accessed from a controller method, for exampl
|
|||
@Controller
|
||||
public class MyController {
|
||||
|
||||
@MessageMapping("/action")
|
||||
public void handle(SimpMessageHeaderAccessor headerAccessor) {
|
||||
Map<String, Object> attrs = headerAccessor.getSessionAttributes();
|
||||
// ...
|
||||
}
|
||||
@MessageMapping("/action")
|
||||
public void handle(SimpMessageHeaderAccessor headerAccessor) {
|
||||
Map<String, Object> attrs = headerAccessor.getSessionAttributes();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
|
@ -1987,38 +1980,38 @@ scope proxy mode for WebSocket-scoped beans:
|
|||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@Component
|
||||
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
|
||||
public class MyBean {
|
||||
@Component
|
||||
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
|
||||
public class MyBean {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// Invoked after dependencies injected
|
||||
}
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// Invoked after dependencies injected
|
||||
}
|
||||
|
||||
// ...
|
||||
// ...
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
// Invoked when the WebSocket session ends
|
||||
}
|
||||
}
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
// Invoked when the WebSocket session ends
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
public class MyController {
|
||||
@Controller
|
||||
public class MyController {
|
||||
|
||||
private final MyBean myBean;
|
||||
private final MyBean myBean;
|
||||
|
||||
@Autowired
|
||||
public MyController(MyBean myBean) {
|
||||
this.myBean = myBean;
|
||||
}
|
||||
@Autowired
|
||||
public MyController(MyBean myBean) {
|
||||
this.myBean = myBean;
|
||||
}
|
||||
|
||||
@MessageMapping("/action")
|
||||
public void handle() {
|
||||
// this.myBean from the current WebSocket session
|
||||
}
|
||||
}
|
||||
@MessageMapping("/action")
|
||||
public void handle() {
|
||||
// this.myBean from the current WebSocket session
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
As with any custom scope, Spring initializes a new `MyBean` instance the first
|
||||
|
|
@ -2205,49 +2198,49 @@ every 30 minutes. This bean can be exported to JMX through Spring's
|
|||
Below is a summary of the available information.
|
||||
|
||||
Client WebSocket Sessions::
|
||||
Current::: indicates how many client sessions there are
|
||||
currently with the count further broken down by WebSocket vs HTTP
|
||||
streaming and polling SockJS sessions.
|
||||
Total::: indicates how many total sessions have been established.
|
||||
Abnormally Closed:::
|
||||
Connect Failures:::: these are sessions that got established but were
|
||||
closed after not having received any messages within 60 seconds. This is
|
||||
usually an indication of proxy or network issues.
|
||||
Send Limit Exceeded:::: sessions closed after exceeding the configured send
|
||||
timeout or the send buffer limits which can occur with slow clients
|
||||
(see previous section).
|
||||
Transport Errors:::: sessions closed after a transport error such as
|
||||
failure to read or write to a WebSocket connection or
|
||||
HTTP request/response.
|
||||
STOMP Frames::: the total number of CONNECT, CONNECTED, and DISCONNECT frames
|
||||
processed indicating how many clients connected on the STOMP level. Note that
|
||||
the DISCONNECT count may be lower when sessions get closed abnormally or when
|
||||
clients close without sending a DISCONNECT frame.
|
||||
Current::: indicates how many client sessions there are
|
||||
currently with the count further broken down by WebSocket vs HTTP
|
||||
streaming and polling SockJS sessions.
|
||||
Total::: indicates how many total sessions have been established.
|
||||
Abnormally Closed:::
|
||||
Connect Failures:::: these are sessions that got established but were
|
||||
closed after not having received any messages within 60 seconds. This is
|
||||
usually an indication of proxy or network issues.
|
||||
Send Limit Exceeded:::: sessions closed after exceeding the configured send
|
||||
timeout or the send buffer limits which can occur with slow clients
|
||||
(see previous section).
|
||||
Transport Errors:::: sessions closed after a transport error such as
|
||||
failure to read or write to a WebSocket connection or
|
||||
HTTP request/response.
|
||||
STOMP Frames::: the total number of CONNECT, CONNECTED, and DISCONNECT frames
|
||||
processed indicating how many clients connected on the STOMP level. Note that
|
||||
the DISCONNECT count may be lower when sessions get closed abnormally or when
|
||||
clients close without sending a DISCONNECT frame.
|
||||
STOMP Broker Relay::
|
||||
TCP Connections::: indicates how many TCP connections on behalf of client
|
||||
WebSocket sessions are established to the broker. This should be equal to the
|
||||
number of client WebSocket sessions + 1 additional shared "system" connection
|
||||
for sending messages from within the application.
|
||||
STOMP Frames::: the total number of CONNECT, CONNECTED, and DISCONNECT frames
|
||||
forwarded to or received from the broker on behalf of clients. Note that a
|
||||
DISCONNECT frame is sent to the broker regardless of how the client WebSocket
|
||||
session was closed. Therefore a lower DISCONNECT frame count is an indication
|
||||
that the broker is pro-actively closing connections, may be because of a
|
||||
heartbeat that didn't arrive in time, an invalid input frame, or other.
|
||||
TCP Connections::: indicates how many TCP connections on behalf of client
|
||||
WebSocket sessions are established to the broker. This should be equal to the
|
||||
number of client WebSocket sessions + 1 additional shared "system" connection
|
||||
for sending messages from within the application.
|
||||
STOMP Frames::: the total number of CONNECT, CONNECTED, and DISCONNECT frames
|
||||
forwarded to or received from the broker on behalf of clients. Note that a
|
||||
DISCONNECT frame is sent to the broker regardless of how the client WebSocket
|
||||
session was closed. Therefore a lower DISCONNECT frame count is an indication
|
||||
that the broker is pro-actively closing connections, may be because of a
|
||||
heartbeat that didn't arrive in time, an invalid input frame, or other.
|
||||
Client Inbound Channel:: stats from thread pool backing the "clientInboundChannel"
|
||||
providing insight into the health of incoming message processing. Tasks queueing
|
||||
up here is an indication the application may be too slow to handle messages.
|
||||
If there I/O bound tasks (e.g. slow database query, HTTP request to 3rd party
|
||||
REST API, etc) consider increasing the thread pool size.
|
||||
providing insight into the health of incoming message processing. Tasks queueing
|
||||
up here is an indication the application may be too slow to handle messages.
|
||||
If there I/O bound tasks (e.g. slow database query, HTTP request to 3rd party
|
||||
REST API, etc) consider increasing the thread pool size.
|
||||
Client Outbound Channel:: stats from the thread pool backing the "clientOutboundChannel"
|
||||
providing insight into the health of broadcasting messages to clients. Tasks
|
||||
queueing up here is an indication clients are too slow to consume messages.
|
||||
One way to address this is to increase the thread pool size to accommodate the
|
||||
number of concurrent slow clients expected. Another option is to reduce the
|
||||
send timeout and send buffer size limits (see the previous section).
|
||||
providing insight into the health of broadcasting messages to clients. Tasks
|
||||
queueing up here is an indication clients are too slow to consume messages.
|
||||
One way to address this is to increase the thread pool size to accommodate the
|
||||
number of concurrent slow clients expected. Another option is to reduce the
|
||||
send timeout and send buffer size limits (see the previous section).
|
||||
SockJS Task Scheduler:: stats from thread pool of the SockJS task scheduler which
|
||||
is used to send heartbeats. Note that when heartbeats are negotiated on the
|
||||
STOMP level the SockJS heartbeats are disabled.
|
||||
is used to send heartbeats. Note that when heartbeats are negotiated on the
|
||||
STOMP level the SockJS heartbeats are disabled.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue