diff --git a/src/docs/asciidoc/core.adoc b/src/docs/asciidoc/core.adoc index 97cc569b52a..23132d1f915 100644 --- a/src/docs/asciidoc/core.adoc +++ b/src/docs/asciidoc/core.adoc @@ -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 diff --git a/src/docs/asciidoc/core/core-beans.adoc b/src/docs/asciidoc/core/core-beans.adoc index 3560ac7279b..d1bd307a352 100644 --- a/src/docs/asciidoc/core/core-beans.adoc +++ b/src/docs/asciidoc/core/core-beans.adoc @@ -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 `` 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 - 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] diff --git a/src/docs/asciidoc/core/core-databuffer-codec.adoc b/src/docs/asciidoc/core/core-databuffer-codec.adoc index 88ee6124fb6..ab7c0b1883f 100644 --- a/src/docs/asciidoc/core/core-databuffer-codec.adoc +++ b/src/docs/asciidoc/core/core-databuffer-codec.adoc @@ -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 diff --git a/src/docs/asciidoc/core/core-resources.adoc b/src/docs/asciidoc/core/core-resources.adoc index c56eae53935..12e66dff14d 100644 --- a/src/docs/asciidoc/core/core-resources.adoc +++ b/src/docs/asciidoc/core/core-resources.adoc @@ -444,8 +444,8 @@ this: ---- com/ foo/ - services.xml - daos.xml + services.xml + daos.xml MessengerService.class ---- diff --git a/src/docs/asciidoc/data-access.adoc b/src/docs/asciidoc/data-access.adoc index 849750729e2..39d56ab8264 100644 --- a/src/docs/asciidoc/data-access.adoc +++ b/src/docs/asciidoc/data-access.adoc @@ -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 creationEvent) { - ... - } - } + @TransactionalEventListener + public void handleOrderCreatedEvent(CreationEvent 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(); + } } ---- diff --git a/src/docs/asciidoc/integration.adoc b/src/docs/asciidoc/integration.adoc index 24f8f770567..dbed4660b7a 100644 --- a/src/docs/asciidoc/integration.adoc +++ b/src/docs/asciidoc/integration.adoc @@ -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. ---- - - - - - + + + + + ---- @@ -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 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> processOrder(Order order) { - // order processing - Message response = MessageBuilder - .withPayload(status) - .setHeader("code", 1234) - .build(); + // order processing + Message 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: - + ---- @@ -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"] ---- + class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/> + 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"] ---- + 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"] ---- + class="org.springframework.cache.jcache.JCacheCacheManager" + p:cache-manager-ref="jCacheManager"/> diff --git a/src/docs/asciidoc/languages.adoc b/src/docs/asciidoc/languages.adoc index 3937f1a6d4d..c4ec56b062f 100644 --- a/src/docs/asciidoc/languages.adoc +++ b/src/docs/asciidoc/languages.adoc @@ -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: diff --git a/src/docs/asciidoc/languages/kotlin.adoc b/src/docs/asciidoc/languages/kotlin.adoc index b7c3e8a559f..0f968189c4f 100644 --- a/src/docs/asciidoc/languages/kotlin.adoc +++ b/src/docs/asciidoc/languages/kotlin.adoc @@ -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 users = client.get().retrieve().bodyToFlux(User.class) + Flux 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() -// or (both are equivalent) -val users : Flux = client.get().retrieve().bodyToFlux() + val users = client.get().retrieve().bodyToFlux() + // or (both are equivalent) + val users : Flux = 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() - registerBean { Bar(it.getBean()) } -} + val context = GenericApplicationContext().apply { + registerBean() + registerBean { Bar(it.getBean()) } + } ---- 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() - bean() - bean("webHandler") { - RouterFunctions.toWebHandler( - ref().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() - } -} + fun beans() = beans { + bean() + bean() + bean("webHandler") { + RouterFunctions.toWebHandler( + ref().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() + } + } ---- In this example, `bean()` is using autowiring by constructor and `ref()` @@ -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 <> 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")} -

${i18n("title")}

-
    - ${users.joinToLine{ "
  • ${i18n("user")} ${it.firstname} ${it.lastname}
  • " }} -
-${include("footer")} -""" + """ + ${include("header")} +

${i18n("title")}

+
    + ${users.joinToLine{ "
  • ${i18n("user")} ${it.firstname} ${it.lastname}
  • " }} +
+ ${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 diff --git a/src/docs/asciidoc/overview.adoc b/src/docs/asciidoc/overview.adoc index 057d5ce1886..37c86ea8864 100644 --- a/src/docs/asciidoc/overview.adoc +++ b/src/docs/asciidoc/overview.adoc @@ -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 diff --git a/src/docs/asciidoc/testing-webtestclient.adoc b/src/docs/asciidoc/testing-webtestclient.adoc index 3346fab2754..0ead66eeb13 100644 --- a/src/docs/asciidoc/testing-webtestclient.adoc +++ b/src/docs/asciidoc/testing-webtestclient.adoc @@ -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 <> 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 result = client.get().uri("/persons/1") - .exchange() - .expectStatus().isOk() - .expectBody(Person.class) - .returnResult(); + EntityExchangeResult 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 result = client.get().uri("/events") - .accept(TEXT_EVENT_STREAM) - .exchange() - .expectStatus().isOk() - .returnResult(MyEvent.class); + FluxExchangeResult 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 eventFux = result.getResponseBody(); + Flux 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(); ---- diff --git a/src/docs/asciidoc/testing.adoc b/src/docs/asciidoc/testing.adoc index f2e13261015..3e805c8621e 100644 --- a/src/docs/asciidoc/testing.adoc +++ b/src/docs/asciidoc/testing.adoc @@ -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] ---- -
- + + - - + + - - + + -
- -
-
+
+ +
+ ---- 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 <>. 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 <> 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 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 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 <>, 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 createMessage(Class 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 createMessage(Class 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 diff --git a/src/docs/asciidoc/web-reactive.adoc b/src/docs/asciidoc/web-reactive.adoc index b80b8bdf154..b3da2ba2e42 100644 --- a/src/docs/asciidoc/web-reactive.adoc +++ b/src/docs/asciidoc/web-reactive.adoc @@ -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 diff --git a/src/docs/asciidoc/web.adoc b/src/docs/asciidoc/web.adoc index 1b689dcdbed..d0c34bb8e12 100644 --- a/src/docs/asciidoc/web.adoc +++ b/src/docs/asciidoc/web.adoc @@ -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 diff --git a/src/docs/asciidoc/web/webflux-webclient.adoc b/src/docs/asciidoc/web/webflux-webclient.adoc index 31edbba93f7..8d49c6af9ab 100644 --- a/src/docs/asciidoc/web/webflux-webclient.adoc +++ b/src/docs/asciidoc/web/webflux-webclient.adoc @@ -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 result = client.get() - .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToMono(Person.class); + Mono 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 result = client.get() - .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) - .retrieve() - .bodyToFlux(Quote.class); + Flux 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 result = client.get() - .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) - .retrieve() - .onStatus(HttpStatus::is4xxServerError, response -> ...) - .onStatus(HttpStatus::is5xxServerError, response -> ...) - .bodyToMono(Person.class); + Mono 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 result = client.get() - .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) - .exchange() - .flatMap(response -> response.bodyToMono(Person.class)); + Mono 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> result = client.get() - .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) - .exchange() - .flatMap(response -> response.toEntity(Person.class)); + Mono> 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 personMono = ... ; + Mono personMono = ... ; - Mono result = client.post() - .uri("/persons/{id}", id) - .contentType(MediaType.APPLICATION_JSON) - .body(personMono, Person.class) - .retrieve() - .bodyToMono(Void.class); + Mono 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 personFlux = ... ; + Flux personFlux = ... ; - Mono result = client.post() - .uri("/persons/{id}", id) - .contentType(MediaType.APPLICATION_STREAM_JSON) - .body(personFlux, Person.class) - .retrieve() - .bodyToMono(Void.class); + Mono 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 result = client.post() - .uri("/persons/{id}", id) - .contentType(MediaType.APPLICATION_JSON) - .syncBody(person) - .retrieve() - .bodyToMono(Void.class); + Mono 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 formData = ... ; + MultiValueMap formData = ... ; - Mono result = client.post() - .uri("/path", id) - .syncBody(formData) - .retrieve() - .bodyToMono(Void.class); + Mono 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 result = client.post() - .uri("/path", id) - .body(fromFormData("k1", "v1").with("k2", "v2")) - .retrieve() - .bodyToMono(Void.class); + Mono 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> parts = builder.build(); + MultiValueMap> parts = builder.build(); -Mono result = client.post() - .uri("/path", id) - .syncBody(parts) - .retrieve() - .bodyToMono(Void.class); + Mono 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 result = client.post() - .uri("/path", id) - .body(fromMultipartData("fieldPart", "value").with("filePart", resource)) - .retrieve() - .bodyToMono(Void.class); + Mono 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 <> 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 <>. @@ -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(); ---- diff --git a/src/docs/asciidoc/web/webflux-websocket.adoc b/src/docs/asciidoc/web/webflux-websocket.adoc index 21bce882494..52c1cf9d80c 100644 --- a/src/docs/asciidoc/web/webflux-websocket.adoc +++ b/src/docs/asciidoc/web/webflux-websocket.adoc @@ -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 handle(WebSocketSession session) { - // ... + @Override + public Mono 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 map = new HashMap<>(); - map.put("/path", new MyWebSocketHandler()); + @Bean + public HandlerMapping handlerMapping() { + Map 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 diff --git a/src/docs/asciidoc/web/webmvc-view.adoc b/src/docs/asciidoc/web/webmvc-view.adoc index f0a312f498c..87df1d259a8 100644 --- a/src/docs/asciidoc/web/webmvc-view.adoc +++ b/src/docs/asciidoc/web/webmvc-view.adoc @@ -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: - + ---- diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index 5415774f41b..a2020f2e2d0 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -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 ---- - - org.springframework.web.context.ContextLoaderListener - + + org.springframework.web.context.ContextLoaderListener + - - contextConfigLocation - /WEB-INF/app-context.xml - + + contextConfigLocation + /WEB-INF/app-context.xml + - - app - org.springframework.web.servlet.DispatcherServlet - - contextConfigLocation - - - 1 - + + app + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + + + 1 + - - app - /app/* - + + app + /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[] getServletConfigClasses() { - return new Class[] { App1Config.class }; - } + @Override + protected Class[] getServletConfigClasses() { + return new Class - - org.springframework.web.context.ContextLoaderListener - + + org.springframework.web.context.ContextLoaderListener + - - contextConfigLocation - /WEB-INF/root-context.xml - + + contextConfigLocation + /WEB-INF/root-context.xml + - - app1 - org.springframework.web.servlet.DispatcherServlet - - contextConfigLocation - /WEB-INF/app1-context.xml - - 1 - + + app1 + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + /WEB-INF/app1-context.xml + + 1 + - - app1 - /app1/* - + + app1 + /app1/* + ---- @@ -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. -* `SourceHttpMessageConverter` converts to/from a javax.xml.transform.Source. +* `ByteArrayHttpMessageConverter` converts byte arrays +* `StringHttpMessageConverter` converts strings +* `FormHttpMessageConverter` converts form data to/from a `MultiValueMap` +* `SourceHttpMessageConverter` converts to/from a `javax.xml.transform.Source`` For more information on these converters, see <>. Also note that if using the MVC namespace or the MVC Java config, a -wider range of message converters are registered by default. See <> 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 <> 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 <> 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 <> instead. [source,xml,indent=0] [subs="verbatim,quotes"] ---- - + - + @@ -2360,11 +2345,10 @@ the MVC namespace or the MVC Java config see <> instead. - - + - + ---- 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 `` element: The above registers a number of Spring MVC <> 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"] ---- - - - - - - - - - - + + + + + + + + + + - + - + ---- @@ -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"] ---- - - - + + + - - + + ---- @@ -4797,7 +4769,6 @@ hook of the Spring `ApplicationContext`: public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { // ... } - } ---- diff --git a/src/docs/asciidoc/web/websocket.adoc b/src/docs/asciidoc/web/websocket.adoc index 7d05ad825db..cccc0dea3a1 100644 --- a/src/docs/asciidoc/web/websocket.adoc +++ b/src/docs/asciidoc/web/websocket.adoc @@ -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"] ---- - + - - - - + + + + - - - + + + - + ---- 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 attrs = headerAccessor.getSessionAttributes(); - // ... - } + @MessageMapping("/action") + public void handle(SimpMessageHeaderAccessor headerAccessor) { + Map 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.