mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
325 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 620bce5527 | |||
| d6e24fdefd | |||
| 8de2a9b74d | |||
| dafd904b14 | |||
| d2f2918bf0 | |||
| 900c45eab5 | |||
| cbe8d73342 | |||
| 07bb95b57f | |||
| 6e354b1209 | |||
| 2482a83d4f | |||
| a88093c699 | |||
| 804ca26a26 | |||
| 494b6abd27 | |||
| 3e4ba75716 | |||
| bd4ebd62eb | |||
| 29bd9b7fdd | |||
| f59f7cca84 | |||
| 3ae7f29995 | |||
| 15624f3929 | |||
| 9733cf8515 | |||
| 08bb796c8c | |||
| dc8a776cae | |||
| 40a79a87ae | |||
| 65460db403 | |||
| 32fcff5249 | |||
| 9991649216 | |||
| 103f57a0a6 | |||
| a2a1a70c32 | |||
| 7795f02988 | |||
| 0e49291982 | |||
| a9edc23533 | |||
| b0726ffbb3 | |||
| f02a2bef8f | |||
| 20fe1bcd99 | |||
| aa0a3bd4d9 | |||
| d5e637a33c | |||
| e913167fa4 | |||
| ca7198c446 | |||
| 6d18ead0b7 | |||
| f49e0e36ff | |||
| 3371c23343 | |||
| da84155d8d | |||
| 60a4766e9e | |||
| 3fb51b0743 | |||
| 25a1bfb093 | |||
| 187b029671 | |||
| 87b1b618fb | |||
| 3b09456991 | |||
| f4eefc62c1 | |||
| 6124a3f0ba | |||
| 7861cfab6c | |||
| 7ec6a9dc54 | |||
| 2ade77304a | |||
| 3a610bcf0d | |||
| 5cc501b9d3 | |||
| afdcaa9d1b | |||
| 543333f5ba | |||
| db48b546ac | |||
| e48292fc07 | |||
| 6456ba9e60 | |||
| c275989323 | |||
| 9486ece0d8 | |||
| 560fec51a1 | |||
| 8fe530c449 | |||
| 5b4f3e871f | |||
| 547a13985a | |||
| 216c4a1c93 | |||
| a3bb9b2fe4 | |||
| c2b2701f23 | |||
| ec87fdb028 | |||
| c1f534548d | |||
| b7883c3e37 | |||
| 5de4f756f3 | |||
| 385c97db9f | |||
| 32cbbfe111 | |||
| ca4a696a70 | |||
| a63c8886c0 | |||
| 2281e42191 | |||
| 55d07566ac | |||
| 2fd06bda3a | |||
| 337d5874b1 | |||
| 400b39da3f | |||
| c5d3bcfb14 | |||
| 9b7f64a102 | |||
| 755721fd21 | |||
| 0ecdf0ba7c | |||
| 81d5e66fe7 | |||
| b95edb549b | |||
| b0b4b47bae | |||
| c9573a4fb6 | |||
| 210dd0392f | |||
| db3570d0cf | |||
| eba1aba9f5 | |||
| 0117a5f4c9 | |||
| 72683766dc | |||
| 3a9896b849 | |||
| eb483d1028 | |||
| 2891dc6409 | |||
| f7440884c9 | |||
| 1a6a4598a1 | |||
| 2a331ab9e2 | |||
| 46c296b154 | |||
| bfd20da9de | |||
| ce4001dd40 | |||
| b254777744 | |||
| d9f9f8b0de | |||
| 4e70e4c81d | |||
| f868486be5 | |||
| a67ac8267f | |||
| 35adf2b1bc | |||
| 6be892e03d | |||
| ab6b504692 | |||
| 515bae9195 | |||
| 7a793588fb | |||
| 1c5fb9a3e8 | |||
| d841af4e37 | |||
| ba8000d5b8 | |||
| d6d3023ed9 | |||
| f96a3d5ee1 | |||
| a53d28edf1 | |||
| a6f7386c9d | |||
| bca63918ab | |||
| 14d539017c | |||
| f4ae18fa89 | |||
| fc5a6db84b | |||
| 9a847dc5a0 | |||
| a82cf2fb7d | |||
| a6258b8b4f | |||
| 0400fc8443 | |||
| df9d09389f | |||
| 3d14970c03 | |||
| 236eea8de9 | |||
| 1b6afd52cb | |||
| 837d3e0ccf | |||
| 4173ceefa5 | |||
| 586235e9e4 | |||
| 4f81ab7e14 | |||
| 7281d44178 | |||
| f965c2f5af | |||
| cf5bbce3f8 | |||
| a80e36fd9f | |||
| 840ae05942 | |||
| b5928977d1 | |||
| 5cb950db3b | |||
| 4b7890315f | |||
| f81b1de2c0 | |||
| d9c98801bc | |||
| 70a2219fac | |||
| 083dd0e19d | |||
| e6553939b2 | |||
| 008d011d37 | |||
| 16ab43d957 | |||
| 6c5129b829 | |||
| e25e6a68cf | |||
| bb96bd4f48 | |||
| 370bf184af | |||
| cbcd8fdeba | |||
| 9bb534e74c | |||
| ec915bfd5d | |||
| 5c04c96d0e | |||
| 7d39fbea16 | |||
| 4ad7deda4c | |||
| 4ec02a7e53 | |||
| 2785ef0887 | |||
| b22e670668 | |||
| 436e9d8cdc | |||
| f474595dee | |||
| 42f60fe490 | |||
| ce76a9b5de | |||
| 6d26b962a9 | |||
| a4cc16051c | |||
| c8b49346b1 | |||
| 34b9ca310d | |||
| ea7b0103c5 | |||
| e3175a2dc9 | |||
| 91316dc11b | |||
| ac8d1df3ab | |||
| f023df703a | |||
| 9ab7cde00e | |||
| fad9b509ab | |||
| 5ddbefecea | |||
| a4bba6aff2 | |||
| 906583c40a | |||
| 3689b3fdd5 | |||
| 4a5bac97b5 | |||
| 45a270b71f | |||
| ef013f7158 | |||
| 818d18aa09 | |||
| 4715ad242e | |||
| e51f660286 | |||
| 9f71c98871 | |||
| dbb09334f3 | |||
| 5953d99d1c | |||
| c9be4f6874 | |||
| fbc63b0e32 | |||
| 5f7df0d14f | |||
| ee832206dc | |||
| 1e295c7bbb | |||
| 4fbba931a5 | |||
| 86f19f6cb6 | |||
| df60919bb0 | |||
| 2f1f36eeda | |||
| 1c0a4f8d78 | |||
| cc376ac5ed | |||
| 10c9d2fcec | |||
| 439ffe2e8a | |||
| 4e55262521 | |||
| ebd2ec57fc | |||
| f536819c5d | |||
| af75c6db2f | |||
| d12619cf1f | |||
| 4c1d227776 | |||
| da02b7a141 | |||
| 17117bd21a | |||
| 0d7494ac52 | |||
| a59a33846e | |||
| 51b8ba334a | |||
| abc2269d97 | |||
| ff385aaf7b | |||
| 1b50ca6bd3 | |||
| 6bae759349 | |||
| e1e8c165db | |||
| 6699833121 | |||
| 095acefd7e | |||
| b62e066b7d | |||
| 9969cb6d83 | |||
| 04b3f5a247 | |||
| c91b47fded | |||
| f2c364b491 | |||
| 170adccf80 | |||
| 98390e1e5e | |||
| 6df8c2678b | |||
| 7f49abb6d1 | |||
| b9bf56e41d | |||
| d0e3e2acfc | |||
| 18d983c686 | |||
| d8abbc501e | |||
| 6ce19ff861 | |||
| c8ef49cc8e | |||
| f61983d908 | |||
| 7615e0b036 | |||
| afe22b84c2 | |||
| 634aba4ab6 | |||
| 26a2d3875f | |||
| 802d083df7 | |||
| f5b43a264a | |||
| 59e4755562 | |||
| 2576aa4063 | |||
| 3fbe762832 | |||
| 611bb0b92e | |||
| 32a8b9b25a | |||
| 97cad6ca8e | |||
| 015f7d8ce1 | |||
| da4e2710b4 | |||
| a368040fd6 | |||
| 5dbd3b0bbf | |||
| 197dbffe20 | |||
| 5341e77796 | |||
| f049a6ec16 | |||
| 7203da7133 | |||
| a21df0cc6d | |||
| 83683a13bb | |||
| 59165dd526 | |||
| 25554d0b21 | |||
| 4f86282b14 | |||
| b3237f3eb6 | |||
| fd68fb115d | |||
| 43e047c523 | |||
| 7fdf775394 | |||
| 59084c6b73 | |||
| 4fcc09a02c | |||
| f5b082d3b3 | |||
| c164759c41 | |||
| 2179b67706 | |||
| fbde98f36e | |||
| 529f8ba786 | |||
| f4676bb41c | |||
| 262332a7c9 | |||
| 8b1709b8f3 | |||
| aee33d8b4b | |||
| 83f03976ea | |||
| ffcd83e3a8 | |||
| 9a522946a5 | |||
| 268d029584 | |||
| 85471d0587 | |||
| 96a1a0dec5 | |||
| 40ac055d11 | |||
| c9a6f4282a | |||
| 6ed6c08ace | |||
| e4c57a9b61 | |||
| b0d8a667a1 | |||
| 9c9ee22c2b | |||
| 639dce6282 | |||
| 8e65834c44 | |||
| 64db939e4a | |||
| 3d83f869d9 | |||
| 088a653318 | |||
| bdb1a81a39 | |||
| c2e7b6341c | |||
| e75556bc7b | |||
| d3d40983d6 | |||
| 32adf77b22 | |||
| 1301c7e95d | |||
| 1a057654b2 | |||
| ca3440cb42 | |||
| 48b22292ff | |||
| e0faaa4807 | |||
| 2e4944198d | |||
| 38a1caefb8 | |||
| e731a0a164 | |||
| 0f2efdbe97 | |||
| feeeab1761 | |||
| b4cf471021 | |||
| 82751141ac | |||
| 9f43ee3304 | |||
| 614c7b0f8e | |||
| cc84533d85 | |||
| 6faf61ba40 | |||
| 80a5019534 | |||
| 03bd02a627 | |||
| f16aa4a9b5 | |||
| a3c9e8d4fc | |||
| 3814f12b67 | |||
| ceb881ab05 | |||
| 871464811c |
@@ -1,16 +0,0 @@
|
||||
# Normalize line endings to LF.
|
||||
* text eol=lf
|
||||
|
||||
# Ensure that line endings for multipart files in spring-web are not modified.
|
||||
*.multipart -text
|
||||
|
||||
# Ensure that line endings for DOS batch files are not modified.
|
||||
*.bat -text
|
||||
|
||||
# Ensure the following are treated as binary.
|
||||
*.gif binary
|
||||
*.jar binary
|
||||
*.jpeg binary
|
||||
*.jpg binary
|
||||
*.png binary
|
||||
*.vsd binary
|
||||
@@ -1,17 +0,0 @@
|
||||
<!--
|
||||
!!! For Security Vulnerabilities, please go to https://spring.io/security-policy !!!
|
||||
-->
|
||||
**Affects:** \<Spring Framework version>
|
||||
|
||||
---
|
||||
<!--
|
||||
Thanks for taking the time to create an issue. Please read the following:
|
||||
|
||||
- Questions should be asked on Stack Overflow.
|
||||
- For bugs, specify affected versions and explain what you are trying to do.
|
||||
- For enhancements, provide context and describe the problem.
|
||||
|
||||
Issue or Pull Request? Create only one, not both. GitHub treats them as the same.
|
||||
If unsure, start with an issue, and if you submit a pull request later, the
|
||||
issue will be closed as superseded.
|
||||
-->
|
||||
@@ -1,29 +0,0 @@
|
||||
name: Backport Bot
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
push:
|
||||
branches:
|
||||
- '*.x'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
- name: Download BackportBot
|
||||
run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar
|
||||
- name: Backport
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_EVENT: ${{ toJSON(github.event) }}
|
||||
run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT"
|
||||
@@ -1,13 +1,10 @@
|
||||
name: "Validate Gradle Wrapper"
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validation:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
+14
-22
@@ -1,42 +1,36 @@
|
||||
# Miscellaneous
|
||||
*.java.hsp
|
||||
*.sonarj
|
||||
*.sw*
|
||||
.DS_Store
|
||||
.settings
|
||||
.springBeans
|
||||
bin
|
||||
build.sh
|
||||
integration-repo
|
||||
ivy-cache
|
||||
argfile*
|
||||
activemq-data/
|
||||
classes/
|
||||
|
||||
# Log files
|
||||
jxl.log
|
||||
jmx.log
|
||||
derby.log
|
||||
|
||||
# Gradle artifacts
|
||||
spring-test/test-output/
|
||||
.gradle
|
||||
.gradletasknamecache
|
||||
argfile*
|
||||
pom.xml
|
||||
activemq-data/
|
||||
|
||||
classes/
|
||||
/build
|
||||
buildSrc/build
|
||||
/spring-*/build
|
||||
/framework-bom/build
|
||||
/framework-docs/build
|
||||
/integration-tests/build
|
||||
/src/asciidoc/build
|
||||
spring-test/test-output/
|
||||
|
||||
# Maven artifacts
|
||||
pom.xml
|
||||
target/
|
||||
|
||||
# Projects not in this branch
|
||||
integration-tests/
|
||||
spring-core/kotlin-coroutines/
|
||||
|
||||
# Eclipse artifacts, including WTP generated manifests
|
||||
bin
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
spring-*/src/main/java/META-INF/MANIFEST.MF
|
||||
|
||||
# IDEA artifacts and output dirs
|
||||
@@ -47,6 +41,4 @@ spring-*/src/main/java/META-INF/MANIFEST.MF
|
||||
out
|
||||
test-output
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
.gradletasknamecache
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
Juergen Hoeller <jhoeller@vmware.com>
|
||||
Juergen Hoeller <jhoeller@vmware.com> <jhoeller@pivotal.io>
|
||||
Juergen Hoeller <jhoeller@vmware.com> <jhoeller@gopivotal.com>
|
||||
Rossen Stoyanchev <rstoyanchev@vmware.com>
|
||||
Rossen Stoyanchev <rstoyanchev@vmware.com> <rstoyanchev@pivotal.io>
|
||||
Rossen Stoyanchev <rstoyanchev@vmware.com> <rstoyanchev@gopivotal.com>
|
||||
Phillip Webb <pwebb@vmware.com>
|
||||
Phillip Webb <pwebb@vmware.com> <pwebb@pivotal.io>
|
||||
Phillip Webb <pwebb@vmware.com> <pwebb@gopivotal.com>
|
||||
Chris Beams <cbeams@vmware.com>
|
||||
Chris Beams <cbeams@vmware.com> <cbeams@pivotal.io>
|
||||
Chris Beams <cbeams@vmware.com> <cbeams@gopivotal.com>
|
||||
Arjen Poutsma <poutsmaa@vmware.com>
|
||||
Arjen Poutsma <poutsmaa@vmware.com> <apoutsma@pivotal.io>
|
||||
Arjen Poutsma <poutsmaa@vmware.com> <apoutsma@gopivotal.com>
|
||||
Arjen Poutsma <poutsmaa@vmware.com> <poutsma@mac.com>
|
||||
Arjen Poutsma <poutsmaa@vmware.com> <apoutsma@vmware.com>
|
||||
Oliver Drotbohm <odrotbohm@vmware.com>
|
||||
Oliver Drotbohm <odrotbohm@vmware.com> <ogierke@vmware.com>
|
||||
Oliver Drotbohm <odrotbohm@vmware.com> <ogierke@pivotal.io>
|
||||
Oliver Drotbohm <odrotbohm@vmware.com> <ogierke@gopivotal.com>
|
||||
Dave Syer <dsyer@vmware.com>
|
||||
Dave Syer <dsyer@vmware.com> <dsyer@pivotal.io>
|
||||
Dave Syer <dsyer@vmware.com> <dsyer@gopivotal.com>
|
||||
Dave Syer <dsyer@vmware.com> <david_syer@hotmail.com>
|
||||
Andy Clement <aclement@vmware.com>
|
||||
Andy Clement <aclement@vmware.com> <aclement@pivotal.io>
|
||||
Andy Clement <aclement@vmware.com> <aclement@gopivotal.com>
|
||||
Andy Clement <aclement@vmware.com> <andrew.clement@gmail.com>
|
||||
Sam Brannen <sbrannen@vmware.com>
|
||||
Sam Brannen <sbrannen@vmware.com> <sbrannen@pivotal.io>
|
||||
Sam Brannen <sbrannen@vmware.com> <sam@sambrannen.com>
|
||||
Juergen Hoeller <jhoeller@pivotal.io> jhoeller <jhoeller@vmware.com>
|
||||
<jhoeller@pivotal.io> <jhoeller@vmware.com>
|
||||
<jhoeller@pivotal.io> <jhoeller@gopivotal.com>
|
||||
<rstoyanchev@pivotal.io> <rstoyanchev@vmware.com>
|
||||
<rstoyanchev@pivotal.io> <rstoyanchev@gopivotal.com>
|
||||
<pwebb@pivotal.io> <pwebb@vmware.com>
|
||||
<pwebb@pivotal.io> <pwebb@gopivotal.com>
|
||||
<cbeams@pivotal.io> <cbeams@vmware.com>
|
||||
<cbeams@pivotal.io> <cbeams@gopivotal.com>
|
||||
<cbeams@pivotal.io> <cbeams@gmail.com>
|
||||
<apoutsma@pivotal.io> <apoutsma@vmware.com>
|
||||
<apoutsma@pivotal.io> <apoutsma@gopivotal.com>
|
||||
<apoutsma@pivotal.io> <poutsma@mac.com>
|
||||
<ogierke@pivotal.io> <ogierke@vmware.com>
|
||||
<ogierke@pivotal.io> <ogierke@gopivotal.com>
|
||||
<dsyer@pivotal.io> <dsyer@vmware.com>
|
||||
<dsyer@pivotal.io> <dsyer@gopivotal.com>
|
||||
<dsyer@pivotal.io> <david_syer@hotmail.com>
|
||||
<aclement@pivotal.io> <aclement@vmware.com>
|
||||
<aclement@pivotal.io> <aclement@gopivotal.com>
|
||||
<aclement@pivotal.io> <andrew.clement@gmail.com>
|
||||
<dmitry.katsubo@gmail.com> <dmitry.katsubo@gmai.com>
|
||||
Nick Williams <nicholas@nicholaswilliams.net>
|
||||
Nick Williams <nicholas@nicholaswilliams.net> Nicholas Williams <nicholas@nicholaswilliams.net>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=17.0.5-librca
|
||||
+53
-60
@@ -6,9 +6,9 @@ First off, thank you for taking the time to contribute! :+1: :tada:
|
||||
|
||||
* [Code of Conduct](#code-of-conduct)
|
||||
* [How to Contribute](#how-to-contribute)
|
||||
* [Ask questions](#ask-questions)
|
||||
* [Create an Issue](#create-an-issue)
|
||||
* [Issue Lifecycle](#issue-lifecycle)
|
||||
* [Discuss](#discuss)
|
||||
* [Create a Ticket](#create-a-ticket)
|
||||
* [Ticket Lifecycle](#ticket-lifecycle)
|
||||
* [Submit a Pull Request](#submit-a-pull-request)
|
||||
* [Build from Source](#build-from-source)
|
||||
* [Source Code Style](#source-code-style)
|
||||
@@ -22,76 +22,74 @@ Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||
|
||||
### How to Contribute
|
||||
|
||||
#### Ask questions
|
||||
#### Discuss
|
||||
|
||||
If you have a question, check Stack Overflow using
|
||||
[this list of tags](https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-transactions+or+spring-annotations+or+spring-jms+or+spring-el+or+spring-test+or+spring+or+spring-remoting+or+spring-orm+or+spring-jmx+or+spring-cache+or+spring-webflux?tab=Newest). Find an existing discussion, or start a new one if necessary.
|
||||
If you have a question, check StackOverflow using
|
||||
[this list of tags](https://spring.io/questions), organized by Spring project.
|
||||
Find an existing discussion or start a new one if necessary.
|
||||
|
||||
If you believe there is an issue, search through
|
||||
[existing issues](https://github.com/spring-projects/spring-framework/issues) trying a
|
||||
few different ways to find discussions, past or current, that are related to the issue.
|
||||
Reading those discussions helps you to learn about the issue, and helps us to make a
|
||||
decision.
|
||||
If you suspect an issue, perform a search in the
|
||||
[Github issue tracker](https://github.com/spring-projects/spring-framework/issues), using a few different keywords.
|
||||
When you find related issues and discussions, prior or current, it helps you to learn and
|
||||
it helps us to make a decision.
|
||||
|
||||
|
||||
#### Create an Issue
|
||||
#### Create a Ticket
|
||||
|
||||
Reporting an issue or making a feature request is a great way to contribute. Your feedback
|
||||
and the conversations that result from it provide a continuous flow of ideas. However,
|
||||
before creating a ticket, please take the time to [ask and research](#ask-questions) first.
|
||||
and the conversations that result from it provide a continuous flow of ideas.
|
||||
|
||||
If you create an issue after a discussion on Stack Overflow, please provide a description
|
||||
in the issue instead of simply referring to Stack Overflow. The issue tracker is an
|
||||
important place of record for design discussions and should be self-sufficient.
|
||||
Before you create a ticket, please take the time to [research first](#discuss).
|
||||
|
||||
Once you're ready, create an issue on [GitHub](https://github.com/spring-projects/spring-framework/issues).
|
||||
If creating a ticket after a discussion on StackOverflow, please provide a self-sufficient description in the ticket, independent of the details on StackOverview. We understand this is extra work but the issue tracker is an important place of record for design discussions and decisions that can often be referenced long after the fix version, for example to revisit decisions, to understand the origin of a feature, and so on.
|
||||
|
||||
Many issues are caused by subtle behavior, typos, and unintended configuration.
|
||||
Creating a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example)
|
||||
(starting with https://start.spring.io for example) of the problem helps the team
|
||||
quickly triage your issue and get to the core of the problem.
|
||||
When ready create a ticket in the [Github issue tracker](https://github.com/spring-projects/spring-framework/issues).
|
||||
|
||||
#### Issue Lifecycle
|
||||
#### Ticket Lifecycle
|
||||
|
||||
When an issue is first created, it is flagged `waiting-for-triage` waiting for a team
|
||||
member to triage it. Once the issue has been reviewed, the team may ask for further
|
||||
information if needed, and based on the findings, the issue is either assigned a target
|
||||
milestone or is closed with a specific status.
|
||||
When an issue is first created, it may not be assigned and will not have a fix version.
|
||||
Within a day or two, the issue is assigned to a specific committer and the target
|
||||
version is set to "Waiting for Triage". The committer will then review the issue, ask for
|
||||
further information if needed, and based on the findings, the issue is either assigned a fix
|
||||
version or rejected.
|
||||
|
||||
When a fix is ready, the issue is closed and may still be re-opened until the fix is
|
||||
released. After that the issue will typically no longer be reopened. In rare cases if the
|
||||
issue was not at all fixed, the issue may be re-opened. In most cases however any
|
||||
follow-up reports will need to be created as new issues with a fresh description.
|
||||
When a fix is ready, the issue is marked "Resolved" and may still be re-opened. Once a fix
|
||||
is released, the issue is permanently "Closed". If necessary, you will need to create a new,
|
||||
related ticket with a fresh description.
|
||||
|
||||
#### Submit a Pull Request
|
||||
|
||||
You can contribute a source code change by submitting a pull request.
|
||||
|
||||
1. If you have not previously done so, please sign the
|
||||
[Contributor License Agreement](https://cla.spring.io/sign/spring). You will be reminded
|
||||
automatically when you submit the PR.
|
||||
[Contributor License Agreement](https://cla.pivotal.io/sign/spring). You will also be reminded
|
||||
automatically when you submit a pull request.
|
||||
|
||||
1. Should you create an issue first? No, just create the pull request and use the
|
||||
description to provide context and motivation, as you would for an issue. If you want
|
||||
to start a discussion first or have already created an issue, once a pull request is
|
||||
created, we will close the issue as superseded by the pull request, and the discussion
|
||||
about the issue will continue under the pull request.
|
||||
1. For all but the most trivial of contributions, please [create a ticket](#create-a-ticket).
|
||||
The purpose of the ticket is to understand and discuss the underlying issue or feature.
|
||||
We use the GitHub issue tracker as the preferred place of record for conversations and
|
||||
conclusions. In that sense discussions directly under a PR are more implementation detail
|
||||
oriented and transient in nature.
|
||||
|
||||
1. Always check out the `main` branch and submit pull requests against it
|
||||
1. Always check out the `master` branch and submit pull requests against it
|
||||
(for target version see [settings.gradle](settings.gradle)).
|
||||
Backports to prior versions will be considered on a case-by-case basis and reflected as
|
||||
the fix version in the issue tracker.
|
||||
|
||||
1. Use short branch names, preferably based on the GitHub issue (e.g. `22276`), or
|
||||
otherwise using succinct, lower-case, dash (-) delimited names, such as `fix-warnings`.
|
||||
|
||||
1. Choose the granularity of your commits consciously and squash commits that represent
|
||||
multiple edits or corrections of the same logical change. See
|
||||
[Rewriting History section of Pro Git](https://git-scm.com/book/en/Git-Tools-Rewriting-History)
|
||||
for an overview of streamlining the commit history.
|
||||
for an overview of streamlining commit history.
|
||||
|
||||
1. Format commit messages using 55 characters for the subject line, 72 characters per line
|
||||
for the description, followed by the issue fixed, e.g. `Closes gh-22276`. See the
|
||||
1. Format commit messages using 55 characters for the subject line, 72 lines for the
|
||||
description, followed by the issue fixed, e.g. `Fixes #22276`.
|
||||
See the
|
||||
[Commit Guidelines section of Pro Git](https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines)
|
||||
for best practices around commit messages, and use `git log` to see some examples.
|
||||
for best practices around commit messages and use `git log` to see some examples.
|
||||
|
||||
1. If there is a prior issue, reference the GitHub issue number in the description of the
|
||||
pull request.
|
||||
1. List the GitHub issue number in the PR description.
|
||||
|
||||
If accepted, your contribution may be heavily modified as needed prior to merging.
|
||||
You will likely retain author attribution for your Git commits granted that the bulk of
|
||||
@@ -101,13 +99,6 @@ If asked to make corrections, simply push the changes against the same branch, a
|
||||
pull request will be updated. In other words, you do not need to create a new pull request
|
||||
when asked to make changes.
|
||||
|
||||
#### Participate in Reviews
|
||||
|
||||
Helping to review pull requests is another great way to contribute. Your feedback
|
||||
can help to shape the implementation of new features. When reviewing pull requests,
|
||||
however, please refrain from approving or rejecting a PR unless you are a core
|
||||
committer for the Spring Framework.
|
||||
|
||||
### Build from Source
|
||||
|
||||
See the [Build from Source](https://github.com/spring-projects/spring-framework/wiki/Build-from-Source)
|
||||
@@ -119,17 +110,19 @@ source code into your IDE.
|
||||
The wiki pages
|
||||
[Code Style](https://github.com/spring-projects/spring-framework/wiki/Code-Style) and
|
||||
[IntelliJ IDEA Editor Settings](https://github.com/spring-projects/spring-framework/wiki/IntelliJ-IDEA-Editor-Settings)
|
||||
define the source file coding standards we use along with some IDEA editor settings we customize.
|
||||
defines the source file coding standards we use along with some IDEA editor settings we customize.
|
||||
|
||||
### Reference Docs
|
||||
|
||||
The reference documentation is in the [framework-docs/src/docs/asciidoc](framework-docs/src/docs/asciidoc) directory, in
|
||||
The reference documentation is in the [src/docs/asciidoc](src/docs/asciidoc) directory and, in
|
||||
[Asciidoctor](https://asciidoctor.org/) format. For trivial changes, you may be able to browse,
|
||||
edit source files, and submit directly from GitHub.
|
||||
|
||||
When making changes locally, execute `./gradlew :framework-docs:asciidoctor` and then browse the result under
|
||||
`framework-docs/build/docs/ref-docs/html5/index.html`.
|
||||
|
||||
Asciidoctor also supports live editing. For more details see
|
||||
[AsciiDoc Tooling](https://docs.asciidoctor.org/asciidoctor/latest/tooling/).
|
||||
When making changes locally, use `./gradlew asciidoctor` and then browse the result under
|
||||
`build/asciidoc/html5/index.html`.
|
||||
|
||||
Asciidoctor also supports live editing. For more details read
|
||||
[Editing AsciiDoc with Live Preview](https://asciidoctor.org/docs/editing-asciidoc-with-live-preview/).
|
||||
Note that if you choose the
|
||||
[System Monitor](https://asciidoctor.org/docs/editing-asciidoc-with-live-preview/#using-a-system-monitor)
|
||||
option, you can find a Guardfile under `src/docs/asciidoc`.
|
||||
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,6 +1,6 @@
|
||||
# <img src="framework-docs/src/docs/spring-framework.png" width="80" height="80"> Spring Framework [](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build") [](https://ge.spring.io/scans?search.rootProjectNames=spring)
|
||||
# <img src="src/docs/asciidoc/images/spring-framework.png" width="80" height="80"> Spring Framework
|
||||
|
||||
This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring".
|
||||
This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects is often referred to simply as "Spring".
|
||||
|
||||
Spring provides everything required beyond the Java programming language for creating enterprise applications for a wide range of scenarios and architectures. Please read the [Overview](https://docs.spring.io/spring/docs/current/spring-framework-reference/overview.html#spring-introduction) section as reference for a more complete introduction.
|
||||
|
||||
@@ -14,20 +14,12 @@ For access to artifacts or a distribution zip, see the [Spring Framework Artifac
|
||||
|
||||
## Documentation
|
||||
|
||||
The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) and [source](framework-docs/src/docs/asciidoc)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an
|
||||
The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) and [source](src/docs/asciidoc)), Github [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an
|
||||
[API reference](https://docs.spring.io/spring-framework/docs/current/javadoc-api/). There are also [guides and tutorials](https://spring.io/guides) across Spring projects.
|
||||
|
||||
## Micro-Benchmarks
|
||||
|
||||
See the [Micro-Benchmarks](https://github.com/spring-projects/spring-framework/wiki/Micro-Benchmarks) wiki page.
|
||||
|
||||
## Build from Source
|
||||
|
||||
See the [Build from Source](https://github.com/spring-projects/spring-framework/wiki/Build-from-Source) wiki page and the [CONTRIBUTING.md](CONTRIBUTING.md) file.
|
||||
|
||||
## Continuous Integration Builds
|
||||
|
||||
Information regarding CI builds can be found in the [Spring Framework Concourse pipeline](ci/README.adoc) documentation.
|
||||
See the [Build from Source](https://github.com/spring-projects/spring-framework/wiki/Build-from-Source) Wikipedia page and the [CONTRIBUTING.md](CONTRIBUTING.md) file.
|
||||
|
||||
## Stay in Touch
|
||||
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Please see the
|
||||
[Spring Framework Versions](https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions)
|
||||
wiki page.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please see https://spring.io/security-policy.
|
||||
+276
-131
@@ -1,177 +1,322 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven { url "https://repo.spring.io/plugins-release" }
|
||||
}
|
||||
dependencies {
|
||||
classpath("io.spring.gradle:propdeps-plugin:0.0.9.RELEASE")
|
||||
classpath("org.asciidoctor:asciidoctorj-pdf:1.5.0-alpha.16")
|
||||
}
|
||||
}
|
||||
|
||||
// 3rd party plugin repositories can be configured in settings.gradle
|
||||
plugins {
|
||||
id 'io.spring.nohttp' version '0.0.10'
|
||||
id 'io.freefair.aspectj' version '6.5.0.3' apply false
|
||||
// kotlinVersion is managed in gradle.properties
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
|
||||
id 'org.jetbrains.dokka' version '1.7.20'
|
||||
id 'org.asciidoctor.jvm.convert' version '3.3.2' apply false
|
||||
id 'org.asciidoctor.jvm.pdf' version '3.3.2' apply false
|
||||
id 'org.unbroken-dome.xjc' version '2.0.0' apply false
|
||||
id 'com.github.ben-manes.versions' version '0.42.0'
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.2' apply false
|
||||
id 'de.undercouch.download' version '5.1.0'
|
||||
id 'me.champeau.jmh' version '0.6.6' apply false
|
||||
id "io.spring.dependency-management" version "1.0.7.RELEASE" apply false
|
||||
id "org.jetbrains.kotlin.jvm" version "1.2.71" apply false
|
||||
id "org.jetbrains.dokka" version "0.9.18"
|
||||
id "org.asciidoctor.convert" version "1.5.8"
|
||||
}
|
||||
|
||||
ext {
|
||||
moduleProjects = subprojects.findAll { it.name.startsWith("spring-") }
|
||||
javaProjects = subprojects - project(":framework-bom") - project(":framework-platform")
|
||||
linkHomepage = "https://spring.io/projects/spring-framework"
|
||||
linkCi = "https://build.spring.io/browse/SPR"
|
||||
linkIssue = "https://github.com/spring-projects/spring-framework/issues"
|
||||
linkScmUrl = "https://github.com/spring-projects/spring-framework"
|
||||
linkScmConnection = "scm:git:git://github.com/spring-projects/spring-framework.git"
|
||||
linkScmDevConnection = "scm:git:ssh://git@github.com:spring-projects/spring-framework.git"
|
||||
|
||||
moduleProjects = subprojects.findAll {
|
||||
!it.name.equals("spring-build-src") && !it.name.equals("spring-framework-bom")
|
||||
}
|
||||
|
||||
aspectjVersion = "1.9.6"
|
||||
freemarkerVersion = "2.3.28"
|
||||
groovyVersion = "2.5.9"
|
||||
hsqldbVersion = "2.4.1"
|
||||
jackson2Version = "2.9.9"
|
||||
jettyVersion = "9.4.31.v20200723"
|
||||
junit5Version = "5.3.2"
|
||||
kotlinVersion = "1.2.71"
|
||||
log4jVersion = "2.11.2"
|
||||
nettyVersion = "4.1.51.Final"
|
||||
reactorVersion = "Californium-SR23"
|
||||
rxjavaVersion = "1.3.8"
|
||||
rxjavaAdapterVersion = "1.2.1"
|
||||
rxjava2Version = "2.2.19"
|
||||
slf4jVersion = "1.7.30" // spring-jcl + consistent 3rd party deps
|
||||
tiles3Version = "3.0.8"
|
||||
tomcatVersion = "9.0.37"
|
||||
undertowVersion = "2.0.32.Final"
|
||||
|
||||
gradleScriptDir = "${rootProject.projectDir}/gradle"
|
||||
withoutJclOverSlf4j = {
|
||||
exclude group: "org.slf4j", module: "jcl-over-slf4j"
|
||||
}
|
||||
}
|
||||
|
||||
configure(allprojects) { project ->
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://repo.spring.io/milestone"
|
||||
content {
|
||||
// Netty 5 optional support
|
||||
includeGroup 'io.projectreactor.netty'
|
||||
}
|
||||
}
|
||||
maven { url "https://repo.spring.io/libs-spring-framework-build" }
|
||||
if (version.contains('-')) {
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
}
|
||||
if (version.endsWith('-SNAPSHOT')) {
|
||||
maven { url "https://repo.spring.io/snapshot" }
|
||||
}
|
||||
}
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
cacheChangingModulesFor 0, "seconds"
|
||||
cacheDynamicVersionsFor 0, "seconds"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configure([rootProject] + javaProjects) { project ->
|
||||
group = "org.springframework"
|
||||
version = qualifyVersionIfNecessary(version)
|
||||
|
||||
apply plugin: "java"
|
||||
apply plugin: "java-test-fixtures"
|
||||
apply plugin: "kotlin"
|
||||
apply plugin: "checkstyle"
|
||||
apply plugin: 'org.springframework.build.conventions'
|
||||
apply from: "${rootDir}/gradle/toolchains.gradle"
|
||||
apply from: "${rootDir}/gradle/ide.gradle"
|
||||
apply plugin: "propdeps"
|
||||
apply plugin: "test-source-set-dependencies"
|
||||
apply plugin: "io.spring.dependency-management"
|
||||
apply from: "${gradleScriptDir}/ide.gradle"
|
||||
|
||||
configurations {
|
||||
dependencyManagement {
|
||||
canBeConsumed = false
|
||||
canBeResolved = false
|
||||
visible = false
|
||||
dependencyManagement {
|
||||
resolutionStrategy {
|
||||
cacheChangingModulesFor 0, "seconds"
|
||||
}
|
||||
applyMavenExclusions = false
|
||||
generatedPomCustomization {
|
||||
enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
// Check for updates every build
|
||||
resolutionStrategy.cacheChangingModulesFor 0, "seconds"
|
||||
|
||||
// Consistent slf4j version (e.g. clashes between slf4j versions)
|
||||
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
|
||||
if (details.requested.group == "org.slf4j") {
|
||||
details.useVersion slf4jVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def commonCompilerArgs =
|
||||
["-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann",
|
||||
"-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides",
|
||||
"-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options"]
|
||||
|
||||
compileJava.options*.compilerArgs = commonCompilerArgs +
|
||||
["-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes",
|
||||
"-Xlint:deprecation", "-Xlint:unchecked", "-Werror"]
|
||||
|
||||
compileTestJava.options*.compilerArgs = commonCompilerArgs +
|
||||
["-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes",
|
||||
"-Xlint:-deprecation", "-Xlint:-unchecked"]
|
||||
|
||||
compileJava {
|
||||
sourceCompatibility = 1.8 // can be switched to 11 for testing
|
||||
targetCompatibility = 1.8
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
compileTestJava {
|
||||
sourceCompatibility = 1.8 // can be switched to 11 for testing
|
||||
targetCompatibility = 1.8
|
||||
options.encoding = "UTF-8"
|
||||
options.compilerArgs += "-parameters"
|
||||
}
|
||||
|
||||
compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = ["-Xjsr305=strict"]
|
||||
apiVersion = "1.1"
|
||||
languageVersion = "1.1"
|
||||
}
|
||||
}
|
||||
|
||||
compileTestKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = ["-Xjsr305=strict"]
|
||||
}
|
||||
matching { it.name.endsWith("Classpath") }.all { it.extendsFrom(dependencyManagement) }
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
include(["**/*Tests.class", "**/*Test.class"])
|
||||
systemProperty("java.awt.headless", "true")
|
||||
systemProperty("testGroups", project.properties.get("testGroups"))
|
||||
systemProperty("io.netty.leakDetection.level", "paranoid")
|
||||
systemProperty("io.netty5.leakDetectionLevel", "paranoid")
|
||||
systemProperty("io.netty5.leakDetection.targetRecords", "32")
|
||||
systemProperty("io.netty5.buffer.lifecycleTracingEnabled", "true")
|
||||
systemProperty("io.netty5.buffer.leakDetectionEnabled", "true")
|
||||
jvmArgs(["--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens=java.base/java.util=ALL-UNNAMED"])
|
||||
scanForTestClasses = false
|
||||
include(["**/*Tests.class", "**/*Test.class"])
|
||||
// Since we set scanForTestClasses to false, we need to filter out inner
|
||||
// classes with the "$" pattern; otherwise, using -Dtest.single=MyTests to
|
||||
// run MyTests by itself will fail if MyTests contains any inner classes.
|
||||
exclude(["**/Abstract*.class", '**/*$*'])
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = "10.4"
|
||||
configDirectory.set(rootProject.file("src/checkstyle"))
|
||||
toolVersion = "8.38"
|
||||
configDir = rootProject.file("src/checkstyle")
|
||||
}
|
||||
|
||||
tasks.named("checkstyleMain").configure {
|
||||
maxHeapSize = "1g"
|
||||
}
|
||||
|
||||
tasks.named("checkstyleTest").configure {
|
||||
maxHeapSize = "1g"
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://repo.spring.io/libs-spring-framework-build" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
dependencyManagement(enforcedPlatform(dependencies.project(path: ":framework-platform")))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-params")
|
||||
testImplementation("org.junit.platform:junit-platform-suite-api")
|
||||
testImplementation("org.mockito:mockito-core")
|
||||
testImplementation("org.mockito:mockito-junit-jupiter")
|
||||
testImplementation("io.mockk:mockk")
|
||||
testImplementation("org.assertj:assertj-core")
|
||||
// Pull in the latest JUnit 5 Launcher API to ensure proper support in IDEs.
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-suite-engine")
|
||||
testRuntimeOnly("org.apache.logging.log4j:log4j-core")
|
||||
testRuntimeOnly("org.apache.logging.log4j:log4j-jul")
|
||||
testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j-impl")
|
||||
testCompile("junit:junit:4.12") {
|
||||
exclude group: "org.hamcrest", module: "hamcrest-core"
|
||||
}
|
||||
testCompile("org.mockito:mockito-core:2.28.2") {
|
||||
exclude group: "org.hamcrest", module: "hamcrest-core"
|
||||
}
|
||||
testCompile("com.nhaarman:mockito-kotlin:1.6.0") {
|
||||
exclude module: "kotlin-stdlib"
|
||||
exclude module: "kotlin-reflect"
|
||||
exclude module: "mockito-core"
|
||||
}
|
||||
testCompile("org.hamcrest:hamcrest-all:1.3")
|
||||
testRuntime("org.apache.logging.log4j:log4j-core:${log4jVersion}")
|
||||
testRuntime("org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}")
|
||||
testRuntime("org.apache.logging.log4j:log4j-jul:${log4jVersion}")
|
||||
// JSR-305 only used for non-required meta-annotations
|
||||
compileOnly("com.google.code.findbugs:jsr305")
|
||||
testCompileOnly("com.google.code.findbugs:jsr305")
|
||||
checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.31")
|
||||
compileOnly("com.google.code.findbugs:jsr305:3.0.2")
|
||||
testCompileOnly("com.google.code.findbugs:jsr305:3.0.2")
|
||||
checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.7")
|
||||
}
|
||||
|
||||
ext.javadocLinks = [
|
||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||
"https://jakarta.ee/specifications/platform/9/apidocs/",
|
||||
"https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ
|
||||
"https://www.ibm.com/docs/api/v1/content/SSEQTP_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/",
|
||||
"https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/",
|
||||
"https://docs.jboss.org/jbossas/javadoc/7.1.2.Final/",
|
||||
"https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://fasterxml.github.io/jackson-core/javadoc/2.10/",
|
||||
"https://fasterxml.github.io/jackson-databind/javadoc/2.10/",
|
||||
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.10/",
|
||||
"https://hc.apache.org/httpcomponents-client-5.2.x/current/httpclient5/apidocs/",
|
||||
"https://projectreactor.io/docs/test/release/api/",
|
||||
"https://junit.org/junit4/javadoc/4.13.2/",
|
||||
// TODO Uncomment link to JUnit 5 docs once we have sorted out
|
||||
// the following warning in the build.
|
||||
//
|
||||
// warning: The code being documented uses packages in the unnamed module, but the packages defined in https://junit.org/junit5/docs/5.9.1/api/ are in named modules.
|
||||
//
|
||||
// "https://junit.org/junit5/docs/5.9.1/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
// The external Javadoc link for JSR 305 must come last to ensure that types from
|
||||
// JSR 250 (such as @PostConstruct) are still supported. This is due to the fact
|
||||
// that JSR 250 and JSR 305 both define types in javax.annotation, which results
|
||||
// in a split package, and the javadoc tool does not support split packages
|
||||
// across multiple external Javadoc sites.
|
||||
"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/"
|
||||
"https://docs.oracle.com/javase/8/docs/api/",
|
||||
"https://docs.oracle.com/javaee/7/api/",
|
||||
"https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ
|
||||
"https://www.ibm.com/support/knowledgecenter/SS7JFU_8.5.5/com.ibm.websphere.javadoc.doc/web/apidocs/",
|
||||
"https://glassfish.java.net/nonav/docs/v3/api/",
|
||||
"https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/",
|
||||
"https://docs.jboss.org/jbossas/javadoc/7.1.2.Final/",
|
||||
"https://tiles.apache.org/tiles-request/apidocs/",
|
||||
"https://tiles.apache.org/framework/apidocs/",
|
||||
"https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
|
||||
"https://www.ehcache.org/apidocs/2.10.4/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://fasterxml.github.io/jackson-core/javadoc/2.9/",
|
||||
"https://fasterxml.github.io/jackson-databind/javadoc/2.9/",
|
||||
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.9/",
|
||||
"https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/",
|
||||
"https://junit.org/junit4/javadoc/4.12/",
|
||||
"https://junit.org/junit5/docs/${junit5Version}/api/"
|
||||
] as String[]
|
||||
}
|
||||
|
||||
configure(moduleProjects) { project ->
|
||||
apply from: "${rootDir}/gradle/spring-module.gradle"
|
||||
configure(subprojects - project(":spring-build-src")) { subproject ->
|
||||
apply from: "${gradleScriptDir}/publish-maven.gradle"
|
||||
|
||||
jar {
|
||||
manifest.attributes["Implementation-Title"] = subproject.name
|
||||
manifest.attributes["Implementation-Version"] = subproject.version
|
||||
manifest.attributes["Automatic-Module-Name"] = subproject.name.replace('-', '.') // for Jigsaw
|
||||
manifest.attributes["Created-By"] =
|
||||
"${System.getProperty("java.version")} (${System.getProperty("java.specification.vendor")})"
|
||||
|
||||
from("${rootProject.projectDir}/src/docs/dist") {
|
||||
include "license.txt"
|
||||
include "notice.txt"
|
||||
into "META-INF"
|
||||
expand(copyright: new Date().format("yyyy"), version: project.version)
|
||||
}
|
||||
}
|
||||
|
||||
javadoc {
|
||||
description = "Generates project-level javadoc for use in -javadoc jar"
|
||||
|
||||
options.encoding = "UTF-8"
|
||||
options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED
|
||||
options.author = true
|
||||
options.header = project.name
|
||||
options.use = true
|
||||
options.links(project.ext.javadocLinks)
|
||||
options.addStringOption("Xdoclint:none", "-quiet")
|
||||
|
||||
// Suppress warnings due to cross-module @see and @link references.
|
||||
// Note that global 'api' task does display all warnings.
|
||||
logging.captureStandardError LogLevel.INFO
|
||||
logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message
|
||||
}
|
||||
|
||||
task sourcesJar(type: Jar, dependsOn: classes) {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
classifier = "sources"
|
||||
from sourceSets.main.allSource
|
||||
// Don't include or exclude anything explicitly by default. See SPR-12085.
|
||||
}
|
||||
|
||||
task javadocJar(type: Jar) {
|
||||
classifier = "javadoc"
|
||||
from javadoc
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives sourcesJar
|
||||
archives javadocJar
|
||||
}
|
||||
}
|
||||
|
||||
configure(rootProject) {
|
||||
description = "Spring Framework"
|
||||
|
||||
apply plugin: "io.spring.nohttp"
|
||||
apply plugin: 'org.springframework.build.api-diff'
|
||||
apply plugin: "groovy"
|
||||
apply from: "${gradleScriptDir}/jdiff.gradle"
|
||||
apply from: "${gradleScriptDir}/docs.gradle"
|
||||
|
||||
nohttp {
|
||||
source.exclude "**/test-output/**"
|
||||
allowlistFile = project.file("src/nohttp/allowlist.lines")
|
||||
def rootPath = file(rootDir).toPath()
|
||||
def projectDirs = allprojects.collect { it.projectDir } + "${rootDir}/buildSrc"
|
||||
projectDirs.forEach { dir ->
|
||||
[ 'bin', 'build', 'out', '.settings' ]
|
||||
.collect { rootPath.relativize(new File(dir, it).toPath()) }
|
||||
.forEach { source.exclude "$it/**" }
|
||||
[ '.classpath', '.project' ]
|
||||
.collect { rootPath.relativize(new File(dir, it).toPath()) }
|
||||
.forEach { source.exclude "$it" }
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "io.projectreactor:reactor-bom:${reactorVersion}"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("checkstyleNohttp").configure {
|
||||
maxHeapSize = "1g"
|
||||
// Don't publish the default jar for the root project
|
||||
configurations.archives.artifacts.clear()
|
||||
|
||||
dependencies { // for integration tests
|
||||
testCompile(project(":spring-aop"))
|
||||
testCompile(project(":spring-beans"))
|
||||
testCompile(project(":spring-context"))
|
||||
testCompile(project(":spring-core"))
|
||||
testCompile(project(":spring-expression"))
|
||||
testCompile(project(":spring-jdbc"))
|
||||
testCompile(project(":spring-orm"))
|
||||
testCompile(project(":spring-test"))
|
||||
testCompile(project(":spring-tx"))
|
||||
testCompile(project(":spring-web"))
|
||||
testCompile("javax.inject:javax.inject:1")
|
||||
testCompile("javax.resource:javax.resource-api:1.7.1")
|
||||
testCompile("javax.servlet:javax.servlet-api:3.1.0")
|
||||
testCompile("org.aspectj:aspectjweaver:${aspectjVersion}")
|
||||
testCompile("org.hsqldb:hsqldb:${hsqldbVersion}")
|
||||
testCompile("org.hibernate:hibernate-core:5.1.17.Final")
|
||||
}
|
||||
|
||||
artifacts {
|
||||
archives docsZip
|
||||
archives schemaZip
|
||||
archives distZip
|
||||
}
|
||||
|
||||
wrapper {
|
||||
doLast() {
|
||||
def gradleOpts = "-XX:MaxMetaspaceSize=1024m -Xmx1024m"
|
||||
def gradleBatOpts = "$gradleOpts -XX:MaxHeapSize=256m"
|
||||
File wrapperFile = file("gradlew")
|
||||
wrapperFile.text = wrapperFile.text.replace("DEFAULT_JVM_OPTS=",
|
||||
"GRADLE_OPTS=\"$gradleOpts \$GRADLE_OPTS\"\nDEFAULT_JVM_OPTS=")
|
||||
File wrapperBatFile = file("gradlew.bat")
|
||||
wrapperBatFile.text = wrapperBatFile.text.replace("set DEFAULT_JVM_OPTS=",
|
||||
"set GRADLE_OPTS=$gradleBatOpts %GRADLE_OPTS%\nset DEFAULT_JVM_OPTS=")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Support publication of artifacts versioned by topic branch.
|
||||
* CI builds supply `-P BRANCH_NAME=<TOPIC>` to gradle at build time.
|
||||
* If <TOPIC> starts with 'SPR-', change version
|
||||
* from BUILD-SNAPSHOT => <TOPIC>-SNAPSHOT
|
||||
* e.g. 3.2.1.BUILD-SNAPSHOT => 3.2.1.SPR-1234-SNAPSHOT
|
||||
*/
|
||||
def qualifyVersionIfNecessary(version) {
|
||||
if (rootProject.hasProperty("BRANCH_NAME")) {
|
||||
def qualifier = rootProject.getProperty("BRANCH_NAME")
|
||||
if (qualifier.startsWith("SPR-")) {
|
||||
return version.replace("BUILD", qualifier)
|
||||
}
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# Spring Framework Build
|
||||
|
||||
This folder contains the custom plugins and conventions for the Spring Framework build.
|
||||
They are declared in the `build.gradle` file in this folder.
|
||||
|
||||
## Build Conventions
|
||||
|
||||
The `org.springframework.build.conventions` plugin applies all conventions to the Framework build:
|
||||
|
||||
* Configuring the Java compiler, see `JavaConventions`
|
||||
* Configuring the Kotlin compiler, see `KotlinConventions`
|
||||
* Configuring testing in the build with `TestConventions`
|
||||
|
||||
|
||||
## Build Plugins
|
||||
|
||||
### Optional dependencies
|
||||
|
||||
The `org.springframework.build.optional-dependencies` plugin creates a new `optional`
|
||||
Gradle configuration - it adds the dependencies to the project's compile and runtime classpath
|
||||
but doesn't affect the classpath of dependent projects.
|
||||
This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly`
|
||||
configurations are preferred.
|
||||
|
||||
### API Diff
|
||||
|
||||
This plugin uses the [Gradle JApiCmp](https://github.com/melix/japicmp-gradle-plugin) plugin
|
||||
to generate API Diff reports for each Spring Framework module. This plugin is applied once on the root
|
||||
project and creates tasks in each framework module. Unlike previous versions of this part of the build,
|
||||
there is no need for checking out a specific tag. The plugin will fetch the JARs we want to compare the
|
||||
current working version with. You can generate the reports for all modules or a single module:
|
||||
|
||||
```
|
||||
./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE
|
||||
./gradlew :spring-core:apiDiff -PbaselineVersion=5.1.0.RELEASE
|
||||
```
|
||||
|
||||
The reports are located under `build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/`.
|
||||
|
||||
|
||||
### RuntimeHints Java Agent
|
||||
|
||||
The `spring-core-test` project module contributes the `RuntimeHintsAgent` Java agent.
|
||||
|
||||
The `RuntimeHintsAgentPlugin` Gradle plugin creates a dedicated `"runtimeHintsTest"` test task for each project.
|
||||
This task will detect and execute [tests tagged](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-gradle)
|
||||
with the `"RuntimeHintsTests"` [JUnit tag](https://junit.org/junit5/docs/current/user-guide/#running-tests-tags).
|
||||
In the Spring Framework test suite, those are usually annotated with the `@EnabledIfRuntimeHintsAgent` annotation.
|
||||
|
||||
By default, the agent will instrument all classes located in the `"org.springframework"` package, as they are loaded.
|
||||
The `RuntimeHintsAgentExtension` allows to customize this using a DSL:
|
||||
|
||||
```groovy
|
||||
// this applies the `RuntimeHintsAgentPlugin` to the project
|
||||
plugins {
|
||||
id 'org.springframework.build.runtimehints-agent'
|
||||
}
|
||||
|
||||
// You can configure the agent to include and exclude packages from the instrumentation process.
|
||||
runtimeHintsAgent {
|
||||
includedPackages = ["org.springframework", "io.spring"]
|
||||
excludedPackages = ["org.example"]
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// to use the test infrastructure, the project should also depend on the "spring-core-test" module
|
||||
testImplementation(project(":spring-core-test"))
|
||||
}
|
||||
```
|
||||
|
||||
With this configuration, `./gradlew runtimeHintsTest` will run all tests instrumented by this java agent.
|
||||
The global `./gradlew check` task depends on `runtimeHintsTest`.
|
||||
|
||||
NOTE: the "spring-core-test" module doesn't shade "spring-core" by design, so the agent should never instrument
|
||||
code that doesn't have "spring-core" on its classpath.
|
||||
@@ -1,45 +0,0 @@
|
||||
plugins {
|
||||
id 'java-gradle-plugin'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
ext {
|
||||
def propertiesFile = new File(new File("$projectDir").parentFile, "gradle.properties")
|
||||
propertiesFile.withInputStream {
|
||||
def properties = new Properties()
|
||||
properties.load(it)
|
||||
set("kotlinVersion", properties["kotlinVersion"])
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
|
||||
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}")
|
||||
implementation "me.champeau.gradle:japicmp-gradle-plugin:0.3.0"
|
||||
implementation "org.gradle:test-retry-gradle-plugin:1.4.1"
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
apiDiffPlugin {
|
||||
id = "org.springframework.build.api-diff"
|
||||
implementationClass = "org.springframework.build.api.ApiDiffPlugin"
|
||||
}
|
||||
conventionsPlugin {
|
||||
id = "org.springframework.build.conventions"
|
||||
implementationClass = "org.springframework.build.ConventionsPlugin"
|
||||
}
|
||||
optionalDependenciesPlugin {
|
||||
id = "org.springframework.build.optional-dependencies"
|
||||
implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin"
|
||||
}
|
||||
runtimeHintsAgentPlugin {
|
||||
id = "org.springframework.build.runtimehints-agent"
|
||||
implementationClass = "org.springframework.build.hint.RuntimeHintsAgentPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
org.gradle.caching=true
|
||||
@@ -0,0 +1,10 @@
|
||||
description = "Exposes gradle buildSrc for IDE support"
|
||||
|
||||
apply plugin: "groovy"
|
||||
|
||||
dependencies {
|
||||
compile gradleApi()
|
||||
compile localGroovy()
|
||||
}
|
||||
|
||||
configurations.archives.artifacts.clear()
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build.gradle
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.ProjectDependency;
|
||||
|
||||
/**
|
||||
* Gradle plugin that automatically updates testCompile dependencies to include
|
||||
* the test source sets of project dependencies.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TestSourceSetDependenciesPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.afterEvaluate {
|
||||
Set<ProjectDependency> projectDependencies = new LinkedHashSet<ProjectDependency>()
|
||||
collectProjectDependencies(projectDependencies, project)
|
||||
projectDependencies.each {
|
||||
project.dependencies.add("testCompile", it.dependencyProject.sourceSets.test.output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void collectProjectDependencies(Set<ProjectDependency> projectDependencies, Project project) {
|
||||
for (def configurationName in ["compile", "optional", "provided", "testCompile"]) {
|
||||
Configuration configuration = project.getConfigurations().findByName(configurationName)
|
||||
if (configuration) {
|
||||
configuration.dependencies.findAll { it instanceof ProjectDependency }.each {
|
||||
projectDependencies.add(it)
|
||||
collectProjectDependencies(projectDependencies, it.dependencyProject)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin;
|
||||
|
||||
/**
|
||||
* Plugin to apply conventions to projects that are part of Spring Framework's build.
|
||||
* Conventions are applied in response to various plugins being applied.
|
||||
*
|
||||
* When the {@link JavaBasePlugin} is applied, the conventions in {@link TestConventions}
|
||||
* are applied.
|
||||
* When the {@link JavaBasePlugin} is applied, the conventions in {@link JavaConventions}
|
||||
* are applied.
|
||||
* When the {@link KotlinBasePlugin} is applied, the conventions in {@link KotlinConventions}
|
||||
* are applied.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class ConventionsPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
new JavaConventions().apply(project);
|
||||
new KotlinConventions().apply(project);
|
||||
new TestConventions().apply(project);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.tasks.compile.JavaCompile;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that applies conventions for compiling Java sources in Spring Framework.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Sam Brannen
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class JavaConventions {
|
||||
|
||||
private static final List<String> COMPILER_ARGS;
|
||||
|
||||
private static final List<String> TEST_COMPILER_ARGS;
|
||||
|
||||
static {
|
||||
List<String> commonCompilerArgs = Arrays.asList(
|
||||
"-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann",
|
||||
"-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides",
|
||||
"-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options",
|
||||
"-parameters"
|
||||
);
|
||||
COMPILER_ARGS = new ArrayList<>();
|
||||
COMPILER_ARGS.addAll(commonCompilerArgs);
|
||||
COMPILER_ARGS.addAll(Arrays.asList(
|
||||
"-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation",
|
||||
"-Xlint:unchecked", "-Werror"
|
||||
));
|
||||
TEST_COMPILER_ARGS = new ArrayList<>();
|
||||
TEST_COMPILER_ARGS.addAll(commonCompilerArgs);
|
||||
TEST_COMPILER_ARGS.addAll(Arrays.asList("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes",
|
||||
"-Xlint:-deprecation", "-Xlint:-unchecked"));
|
||||
}
|
||||
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> applyJavaCompileConventions(project));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the common Java compiler options for main sources, test fixture sources, and
|
||||
* test sources.
|
||||
* @param project the current project
|
||||
*/
|
||||
private void applyJavaCompileConventions(Project project) {
|
||||
project.getTasks().withType(JavaCompile.class)
|
||||
.matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME))
|
||||
.forEach(compileTask -> {
|
||||
compileTask.getOptions().setCompilerArgs(COMPILER_ARGS);
|
||||
compileTask.getOptions().setEncoding("UTF-8");
|
||||
});
|
||||
project.getTasks().withType(JavaCompile.class)
|
||||
.matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME)
|
||||
|| compileTask.getName().equals("compileTestFixturesJava"))
|
||||
.forEach(compileTask -> {
|
||||
compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS);
|
||||
compileTask.getOptions().setEncoding("UTF-8");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions;
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
|
||||
|
||||
/**
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class KotlinConventions {
|
||||
|
||||
void apply(Project project) {
|
||||
project.getPlugins().withId("org.jetbrains.kotlin.jvm",
|
||||
(plugin) -> project.getTasks().withType(KotlinCompile.class, this::configure));
|
||||
}
|
||||
|
||||
private void configure(KotlinCompile compile) {
|
||||
KotlinJvmOptions kotlinOptions = compile.getKotlinOptions();
|
||||
kotlinOptions.setApiVersion("1.7");
|
||||
kotlinOptions.setLanguageVersion("1.7");
|
||||
kotlinOptions.setJvmTarget("17");
|
||||
kotlinOptions.setJavaParameters(true);
|
||||
kotlinOptions.setAllWarningsAsErrors(true);
|
||||
List<String> freeCompilerArgs = new ArrayList<>(compile.getKotlinOptions().getFreeCompilerArgs());
|
||||
freeCompilerArgs.addAll(List.of("-Xsuppress-version-warnings", "-Xjsr305=strict", "-opt-in=kotlin.RequiresOptIn"));
|
||||
compile.getKotlinOptions().setFreeCompilerArgs(freeCompilerArgs);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
import org.gradle.testretry.TestRetryPlugin;
|
||||
import org.gradle.testretry.TestRetryTaskExtension;
|
||||
|
||||
/**
|
||||
* Conventions that are applied in the presence of the {@link JavaBasePlugin}. When the
|
||||
* plugin is applied:
|
||||
* <ul>
|
||||
* <li>The {@link TestRetryPlugin Test Retry} plugins is applied so that flaky tests
|
||||
* are retried 3 times when running on the CI.
|
||||
* </ul>
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class TestConventions {
|
||||
|
||||
void apply(Project project) {
|
||||
project.getPlugins().withType(JavaBasePlugin.class, (java) -> configureTestConventions(project));
|
||||
}
|
||||
|
||||
private void configureTestConventions(Project project) {
|
||||
project.getPlugins().apply(TestRetryPlugin.class);
|
||||
project.getTasks().withType(Test.class,
|
||||
(test) -> project.getPlugins().withType(TestRetryPlugin.class, (testRetryPlugin) -> {
|
||||
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);
|
||||
testRetry.getFailOnPassedAfterRetry().set(true);
|
||||
testRetry.getMaxRetries().set(isCi() ? 3 : 0);
|
||||
}));
|
||||
}
|
||||
|
||||
private boolean isCi() {
|
||||
return Boolean.parseBoolean(System.getenv("CI"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.build.api;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import me.champeau.gradle.japicmp.JapicmpPlugin;
|
||||
import me.champeau.gradle.japicmp.JapicmpTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.Dependency;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.publish.maven.plugins.MavenPublishPlugin;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.jvm.tasks.Jar;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that applies the {@code "japicmp-gradle-plugin"}
|
||||
* and create tasks for all subprojects named {@code "spring-*"}, diffing the public API one by one
|
||||
* and creating the reports in {@code "build/reports/api-diff/$OLDVERSION_to_$NEWVERSION/"}.
|
||||
* <p>{@code "./gradlew apiDiff -PbaselineVersion=5.1.0.RELEASE"} will output the
|
||||
* reports for the API diff between the baseline version and the current one for all modules.
|
||||
* You can limit the report to a single module with
|
||||
* {@code "./gradlew :spring-core:apiDiff -PbaselineVersion=5.1.0.RELEASE"}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class ApiDiffPlugin implements Plugin<Project> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ApiDiffPlugin.class);
|
||||
|
||||
public static final String TASK_NAME = "apiDiff";
|
||||
|
||||
private static final String BASELINE_VERSION_PROPERTY = "baselineVersion";
|
||||
|
||||
private static final List<String> PACKAGE_INCLUDES = Collections.singletonList("org.springframework.*");
|
||||
|
||||
private static final URI SPRING_MILESTONE_REPOSITORY = URI.create("https://repo.spring.io/milestone");
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
if (project.hasProperty(BASELINE_VERSION_PROPERTY) && project.equals(project.getRootProject())) {
|
||||
project.getPluginManager().apply(JapicmpPlugin.class);
|
||||
project.getPlugins().withType(JapicmpPlugin.class,
|
||||
plugin -> applyApiDiffConventions(project));
|
||||
}
|
||||
}
|
||||
|
||||
private void applyApiDiffConventions(Project project) {
|
||||
String baselineVersion = project.property(BASELINE_VERSION_PROPERTY).toString();
|
||||
project.subprojects(subProject -> {
|
||||
if (subProject.getName().startsWith("spring-")) {
|
||||
createApiDiffTask(baselineVersion, subProject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createApiDiffTask(String baselineVersion, Project project) {
|
||||
if (isProjectEligible(project)) {
|
||||
// Add Spring Milestone repository for generating diffs against previous milestones
|
||||
project.getRootProject()
|
||||
.getRepositories()
|
||||
.maven(mavenArtifactRepository -> mavenArtifactRepository.setUrl(SPRING_MILESTONE_REPOSITORY));
|
||||
JapicmpTask apiDiff = project.getTasks().create(TASK_NAME, JapicmpTask.class);
|
||||
apiDiff.setDescription("Generates an API diff report with japicmp");
|
||||
apiDiff.setGroup(JavaBasePlugin.DOCUMENTATION_GROUP);
|
||||
|
||||
apiDiff.setOldClasspath(createBaselineConfiguration(baselineVersion, project));
|
||||
TaskProvider<Jar> jar = project.getTasks().withType(Jar.class).named("jar");
|
||||
apiDiff.setNewArchives(project.getLayout().files(jar.get().getArchiveFile().get().getAsFile()));
|
||||
apiDiff.setNewClasspath(getRuntimeClassPath(project));
|
||||
apiDiff.setPackageIncludes(PACKAGE_INCLUDES);
|
||||
apiDiff.setOnlyModified(true);
|
||||
apiDiff.setIgnoreMissingClasses(true);
|
||||
// Ignore Kotlin metadata annotations since they contain
|
||||
// illegal HTML characters and fail the report generation
|
||||
apiDiff.setAnnotationExcludes(Collections.singletonList("@kotlin.Metadata"));
|
||||
|
||||
apiDiff.setHtmlOutputFile(getOutputFile(baselineVersion, project));
|
||||
|
||||
apiDiff.dependsOn(project.getTasks().getByName("jar"));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isProjectEligible(Project project) {
|
||||
return project.getPlugins().hasPlugin(JavaPlugin.class)
|
||||
&& project.getPlugins().hasPlugin(MavenPublishPlugin.class);
|
||||
}
|
||||
|
||||
private Configuration createBaselineConfiguration(String baselineVersion, Project project) {
|
||||
String baseline = String.join(":",
|
||||
project.getGroup().toString(), project.getName(), baselineVersion);
|
||||
Dependency baselineDependency = project.getDependencies().create(baseline + "@jar");
|
||||
Configuration baselineConfiguration = project.getRootProject().getConfigurations().detachedConfiguration(baselineDependency);
|
||||
try {
|
||||
// eagerly resolve the baseline configuration to check whether this is a new Spring module
|
||||
baselineConfiguration.resolve();
|
||||
return baselineConfiguration;
|
||||
}
|
||||
catch (GradleException exception) {
|
||||
logger.warn("Could not resolve {} - assuming this is a new Spring module.", baseline);
|
||||
}
|
||||
return project.getRootProject().getConfigurations().detachedConfiguration();
|
||||
}
|
||||
|
||||
private Configuration getRuntimeClassPath(Project project) {
|
||||
return project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
|
||||
}
|
||||
|
||||
private File getOutputFile(String baseLineVersion, Project project) {
|
||||
Path outDir = Paths.get(project.getRootProject().getBuildDir().getAbsolutePath(),
|
||||
"reports", "api-diff",
|
||||
baseLineVersion + "_to_" + project.getRootProject().getVersion());
|
||||
return project.file(outDir.resolve(project.getName() + ".html").toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build.hint;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.gradle.api.model.ObjectFactory;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
|
||||
/**
|
||||
* Entry point to the DSL extension for the {@link RuntimeHintsAgentPlugin} Gradle plugin.
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class RuntimeHintsAgentExtension {
|
||||
|
||||
private final SetProperty<String> includedPackages;
|
||||
|
||||
private final SetProperty<String> excludedPackages;
|
||||
|
||||
public RuntimeHintsAgentExtension(ObjectFactory objectFactory) {
|
||||
this.includedPackages = objectFactory.setProperty(String.class).convention(Collections.singleton("org.springframework"));
|
||||
this.excludedPackages = objectFactory.setProperty(String.class).convention(Collections.emptySet());
|
||||
}
|
||||
|
||||
public SetProperty<String> getIncludedPackages() {
|
||||
return this.includedPackages;
|
||||
}
|
||||
|
||||
public SetProperty<String> getExcludedPackages() {
|
||||
return this.excludedPackages;
|
||||
}
|
||||
|
||||
String asJavaAgentArgument() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
this.includedPackages.get().forEach(packageName -> builder.append('+').append(packageName).append(','));
|
||||
this.excludedPackages.get().forEach(packageName -> builder.append('-').append(packageName).append(','));
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build.hint;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.tasks.bundling.Jar;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that configures the {@code RuntimeHints} Java agent to test tasks.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class RuntimeHintsAgentPlugin implements Plugin<Project> {
|
||||
|
||||
public static final String RUNTIMEHINTS_TEST_TASK = "runtimeHintsTest";
|
||||
private static final String EXTENSION_NAME = "runtimeHintsAgent";
|
||||
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
|
||||
project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {
|
||||
RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME,
|
||||
RuntimeHintsAgentExtension.class, project.getObjects());
|
||||
Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
|
||||
test.useJUnitPlatform(options -> {
|
||||
options.includeTags("RuntimeHintsTests");
|
||||
});
|
||||
test.include("**/*Tests.class", "**/*Test.class");
|
||||
test.systemProperty("java.awt.headless", "true");
|
||||
});
|
||||
project.afterEvaluate(p -> {
|
||||
Jar jar = project.getRootProject().project("spring-core-test").getTasks().withType(Jar.class).named("jar").get();
|
||||
agentTest.jvmArgs("-javaagent:" + jar.getArchiveFile().get().getAsFile() + "=" + agentExtension.asJavaAgentArgument());
|
||||
});
|
||||
project.getTasks().getByName("check", task -> task.dependsOn(agentTest));
|
||||
});
|
||||
}
|
||||
}
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build.optional;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.tasks.SourceSetContainer;
|
||||
|
||||
/**
|
||||
* A {@code Plugin} that adds support for Maven-style optional dependencies. Creates a new
|
||||
* {@code optional} configuration. The {@code optional} configuration is part of the
|
||||
* project's compile and runtime classpaths but does not affect the classpath of
|
||||
* dependent projects.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class OptionalDependenciesPlugin implements Plugin<Project> {
|
||||
|
||||
/**
|
||||
* Name of the {@code optional} configuration.
|
||||
*/
|
||||
public static final String OPTIONAL_CONFIGURATION_NAME = "optional";
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
Configuration optional = project.getConfigurations().create("optional");
|
||||
optional.setCanBeConsumed(false);
|
||||
optional.setCanBeResolved(false);
|
||||
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
|
||||
SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class)
|
||||
.getSourceSets();
|
||||
sourceSets.all((sourceSet) -> {
|
||||
project.getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName()).extendsFrom(optional);
|
||||
project.getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName()).extendsFrom(optional);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package org.springframework.build.shadow;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.component.ModuleComponentSelector;
|
||||
import org.gradle.api.artifacts.query.ArtifactResolutionQuery;
|
||||
import org.gradle.api.artifacts.result.ArtifactResolutionResult;
|
||||
import org.gradle.api.artifacts.result.ComponentArtifactsResult;
|
||||
import org.gradle.api.artifacts.result.DependencyResult;
|
||||
import org.gradle.api.artifacts.result.ResolutionResult;
|
||||
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.FileCopyDetails;
|
||||
import org.gradle.api.file.FileTree;
|
||||
import org.gradle.api.tasks.Classpath;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.Nested;
|
||||
import org.gradle.api.tasks.Optional;
|
||||
import org.gradle.api.tasks.OutputDirectory;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.jvm.JvmLibrary;
|
||||
import org.gradle.language.base.artifact.SourcesArtifact;
|
||||
|
||||
/**
|
||||
* Gradle task to add source from shadowed jars into our own source jars.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class ShadowSource extends DefaultTask {
|
||||
|
||||
private final DirectoryProperty outputDirectory = getProject().getObjects().directoryProperty();
|
||||
|
||||
private List<Configuration> configurations = new ArrayList<>();
|
||||
|
||||
private final List<Relocation> relocations = new ArrayList<>();
|
||||
|
||||
|
||||
@Classpath
|
||||
@Optional
|
||||
public List<Configuration> getConfigurations() {
|
||||
return this.configurations;
|
||||
}
|
||||
|
||||
public void setConfigurations(List<Configuration> configurations) {
|
||||
this.configurations = configurations;
|
||||
}
|
||||
|
||||
@Nested
|
||||
public List<Relocation> getRelocations() {
|
||||
return this.relocations;
|
||||
}
|
||||
|
||||
public void relocate(String pattern, String destination) {
|
||||
this.relocations.add(new Relocation(pattern, destination));
|
||||
}
|
||||
|
||||
@OutputDirectory
|
||||
DirectoryProperty getOutputDirectory() {
|
||||
return this.outputDirectory;
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void syncSourceJarFiles() {
|
||||
sync(getSourceJarFiles());
|
||||
}
|
||||
|
||||
private List<File> getSourceJarFiles() {
|
||||
List<File> sourceJarFiles = new ArrayList<>();
|
||||
for (Configuration configuration : this.configurations) {
|
||||
ResolutionResult resolutionResult = configuration.getIncoming().getResolutionResult();
|
||||
resolutionResult.getRootComponent().get().getDependencies().forEach(dependency -> {
|
||||
Set<ComponentArtifactsResult> artifactsResults = resolveSourceArtifacts(dependency);
|
||||
for (ComponentArtifactsResult artifactResult : artifactsResults) {
|
||||
artifactResult.getArtifacts(SourcesArtifact.class).forEach(sourceArtifact -> {
|
||||
sourceJarFiles.add(((ResolvedArtifactResult) sourceArtifact).getFile());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return Collections.unmodifiableList(sourceJarFiles);
|
||||
}
|
||||
|
||||
private Set<ComponentArtifactsResult> resolveSourceArtifacts(DependencyResult dependency) {
|
||||
ModuleComponentSelector componentSelector = (ModuleComponentSelector) dependency.getRequested();
|
||||
ArtifactResolutionQuery query = getProject().getDependencies().createArtifactResolutionQuery()
|
||||
.forModule(componentSelector.getGroup(), componentSelector.getModule(), componentSelector.getVersion());
|
||||
return executeQuery(query).getResolvedComponents();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private ArtifactResolutionResult executeQuery(ArtifactResolutionQuery query) {
|
||||
return query.withArtifacts(JvmLibrary.class, SourcesArtifact.class).execute();
|
||||
}
|
||||
|
||||
private void sync(List<File> sourceJarFiles) {
|
||||
getProject().sync(spec -> {
|
||||
spec.into(this.outputDirectory);
|
||||
spec.eachFile(this::relocateFile);
|
||||
spec.filter(this::transformContent);
|
||||
spec.exclude("META-INF/**");
|
||||
spec.setIncludeEmptyDirs(false);
|
||||
sourceJarFiles.forEach(sourceJar -> spec.from(zipTree(sourceJar)));
|
||||
});
|
||||
}
|
||||
|
||||
private void relocateFile(FileCopyDetails details) {
|
||||
String path = details.getPath();
|
||||
for (Relocation relocation : this.relocations) {
|
||||
path = relocation.relocatePath(path);
|
||||
}
|
||||
details.setPath(path);
|
||||
}
|
||||
|
||||
private String transformContent(String content) {
|
||||
for (Relocation relocation : this.relocations) {
|
||||
content = relocation.transformContent(content);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private FileTree zipTree(File sourceJar) {
|
||||
return getProject().zipTree(sourceJar);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A single relocation.
|
||||
*/
|
||||
static class Relocation {
|
||||
|
||||
private final String pattern;
|
||||
|
||||
private final String pathPattern;
|
||||
|
||||
private final String destination;
|
||||
|
||||
private final String pathDestination;
|
||||
|
||||
|
||||
Relocation(String pattern, String destination) {
|
||||
this.pattern = pattern;
|
||||
this.pathPattern = pattern.replace('.', '/');
|
||||
this.destination = destination;
|
||||
this.pathDestination = destination.replace('.', '/');
|
||||
}
|
||||
|
||||
|
||||
@Input
|
||||
public String getPattern() {
|
||||
return this.pattern;
|
||||
}
|
||||
|
||||
@Input
|
||||
public String getDestination() {
|
||||
return this.destination;
|
||||
}
|
||||
|
||||
String relocatePath(String path) {
|
||||
return path.replace(this.pathPattern, this.pathDestination);
|
||||
}
|
||||
|
||||
public String transformContent(String content) {
|
||||
return content.replaceAll("\\b" + this.pattern, this.destination);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
implementation-class=org.springframework.build.gradle.TestSourceSetDependenciesPlugin
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks.
|
||||
The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline
|
||||
for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-6.0.x[Spring Framework 6.0.x].
|
||||
for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.1.x[Spring Framework 5.1.x].
|
||||
|
||||
=== Setting up your development environment
|
||||
|
||||
@@ -51,7 +51,7 @@ The pipeline can be deployed using the following command:
|
||||
|
||||
[source]
|
||||
----
|
||||
$ fly -t spring set-pipeline -p spring-framework-6.0.x -c ci/pipeline.yml -l ci/parameters.yml
|
||||
$ fly -t spring set-pipeline -p spring-framework-5.1.x -c ci/pipeline.yml -l ci/parameters.yml
|
||||
----
|
||||
|
||||
NOTE: This assumes that you have credhub integration configured with the appropriate secrets.
|
||||
|
||||
@@ -15,6 +15,3 @@ changelog:
|
||||
sort: "title"
|
||||
labels:
|
||||
- "type: dependency-upgrade"
|
||||
contributors:
|
||||
exclude:
|
||||
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll"]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM ubuntu:focal-20220302
|
||||
|
||||
ADD setup.sh /setup.sh
|
||||
ADD get-jdk-url.sh /get-jdk-url.sh
|
||||
RUN ./setup.sh java11
|
||||
|
||||
ENV JAVA_HOME /opt/openjdk
|
||||
ENV PATH $JAVA_HOME/bin:$PATH
|
||||
@@ -1,12 +1,8 @@
|
||||
FROM ubuntu:focal-20220922
|
||||
FROM ubuntu:focal-20220302
|
||||
|
||||
ADD setup.sh /setup.sh
|
||||
ADD get-jdk-url.sh /get-jdk-url.sh
|
||||
RUN ./setup.sh
|
||||
|
||||
ENV JAVA_HOME /opt/openjdk/java17
|
||||
ENV JDK17 /opt/openjdk/java17
|
||||
ENV JDK18 /opt/openjdk/java18
|
||||
ENV JDK19 /opt/openjdk/java19
|
||||
RUN ./setup.sh java8
|
||||
|
||||
ENV JAVA_HOME /opt/openjdk
|
||||
ENV PATH $JAVA_HOME/bin:$PATH
|
||||
|
||||
@@ -2,16 +2,13 @@
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
java17)
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/17.0.5+8/bellsoft-jdk17.0.5+8-linux-amd64.tar.gz"
|
||||
java8)
|
||||
echo "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u322-b06/OpenJDK8U-jdk_x64_linux_hotspot_8u322b06.tar.gz"
|
||||
;;
|
||||
java18)
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/18.0.2.1%2B1/bellsoft-jdk18.0.2.1+1-linux-amd64.tar.gz"
|
||||
java11)
|
||||
echo "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.14.1%2B1/OpenJDK11U-jdk_x64_linux_hotspot_11.0.14.1_1.tar.gz"
|
||||
;;
|
||||
java19)
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/19.0.1%2B11/bellsoft-jdk19.0.1+11-linux-amd64.tar.gz"
|
||||
;;
|
||||
*)
|
||||
*)
|
||||
echo $"Unknown java version"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
+7
-12
@@ -14,23 +14,18 @@ rm -rf /var/lib/apt/lists/*
|
||||
|
||||
curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/concourse-java.sh > /opt/concourse-java.sh
|
||||
|
||||
curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.3.2/concourse-release-scripts-0.3.2.jar
|
||||
|
||||
###########################################################
|
||||
# JAVA
|
||||
###########################################################
|
||||
JDK_URL=$( ./get-jdk-url.sh $1 )
|
||||
|
||||
mkdir -p /opt/openjdk
|
||||
pushd /opt/openjdk > /dev/null
|
||||
for jdk in java17 java18 java19
|
||||
do
|
||||
JDK_URL=$( /get-jdk-url.sh $jdk )
|
||||
mkdir $jdk
|
||||
pushd $jdk > /dev/null
|
||||
curl -L ${JDK_URL} | tar zx --strip-components=1
|
||||
test -f bin/java
|
||||
test -f bin/javac
|
||||
popd > /dev/null
|
||||
done
|
||||
popd
|
||||
cd /opt/openjdk
|
||||
curl -L ${JDK_URL} | tar zx --strip-components=1
|
||||
test -f /opt/openjdk/bin/java
|
||||
test -f /opt/openjdk/bin/javac
|
||||
|
||||
###########################################################
|
||||
# GRADLE ENTERPRISE
|
||||
|
||||
+5
-6
@@ -1,14 +1,13 @@
|
||||
email-server: "smtp.svc.pivotal.io"
|
||||
email-from: "ci@spring.io"
|
||||
email-to: ["spring-framework-dev@pivotal.io"]
|
||||
github-repo: "https://github.com/spring-projects/spring-framework.git"
|
||||
github-repo-name: "spring-projects/spring-framework"
|
||||
sonatype-staging-profile: "org.springframework"
|
||||
docker-hub-organization: "springci"
|
||||
artifactory-server: "https://repo.spring.io"
|
||||
branch: "main"
|
||||
milestone: "6.0.x"
|
||||
branch: "5.1.x"
|
||||
milestone: "5.1.x"
|
||||
build-name: "spring-framework"
|
||||
pipeline-name: "spring-framework"
|
||||
concourse-url: "https://ci.spring.io"
|
||||
registry-mirror-host: docker.repo.spring.io
|
||||
registry-mirror-username: ((artifactory-username))
|
||||
registry-mirror-password: ((artifactory-password))
|
||||
task-timeout: 1h00m
|
||||
|
||||
+57
-146
@@ -12,22 +12,21 @@ anchors:
|
||||
SONATYPE_USERNAME: ((sonatype-username))
|
||||
SONATYPE_PASSWORD: ((sonatype-password))
|
||||
SONATYPE_URL: ((sonatype-url))
|
||||
SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile))
|
||||
SONATYPE_STAGING_PROFILE_ID: ((sonatype-staging-profile-id))
|
||||
artifactory-task-params: &artifactory-task-params
|
||||
ARTIFACTORY_SERVER: ((artifactory-server))
|
||||
ARTIFACTORY_USERNAME: ((artifactory-username))
|
||||
ARTIFACTORY_PASSWORD: ((artifactory-password))
|
||||
build-project-task-params: &build-project-task-params
|
||||
privileged: true
|
||||
timeout: ((task-timeout))
|
||||
params:
|
||||
BRANCH: ((branch))
|
||||
<<: *gradle-enterprise-task-params
|
||||
docker-resource-source: &docker-resource-source
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
tag: ((milestone))
|
||||
registry-mirror-vars: ®istry-mirror-vars
|
||||
registry-mirror-host: ((registry-mirror-host))
|
||||
registry-mirror-username: ((registry-mirror-username))
|
||||
registry-mirror-password: ((registry-mirror-password))
|
||||
slack-fail-params: &slack-fail-params
|
||||
text: >
|
||||
:concourse-failed: <https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!>
|
||||
@@ -45,11 +44,6 @@ anchors:
|
||||
GITHUB_TOKEN: ((github-ci-release-token))
|
||||
|
||||
resource_types:
|
||||
- name: registry-image
|
||||
type: registry-image
|
||||
source:
|
||||
repository: concourse/registry-image-resource
|
||||
tag: 1.5.0
|
||||
- name: artifactory-resource
|
||||
type: registry-image
|
||||
source:
|
||||
@@ -65,11 +59,6 @@ resource_types:
|
||||
source:
|
||||
repository: dpb587/github-status-resource
|
||||
tag: master
|
||||
- name: pull-request
|
||||
type: registry-image
|
||||
source:
|
||||
repository: teliaoss/github-pr-resource
|
||||
tag: v0.23.0
|
||||
- name: slack-notification
|
||||
type: registry-image
|
||||
source:
|
||||
@@ -81,6 +70,13 @@ resources:
|
||||
icon: github
|
||||
source:
|
||||
<<: *git-repo-resource-source
|
||||
- name: every-morning
|
||||
type: time
|
||||
icon: alarm
|
||||
source:
|
||||
start: 8:00 AM
|
||||
stop: 9:00 AM
|
||||
location: Europe/Vienna
|
||||
- name: ci-images-git-repo
|
||||
type: git
|
||||
icon: github
|
||||
@@ -89,18 +85,17 @@ resources:
|
||||
branch: ((branch))
|
||||
paths: ["ci/images/*"]
|
||||
- name: ci-image
|
||||
type: registry-image
|
||||
type: docker-image
|
||||
icon: docker
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: ((docker-hub-organization))/spring-framework-ci
|
||||
- name: every-morning
|
||||
type: time
|
||||
icon: alarm
|
||||
- name: ci-image-jdk11
|
||||
type: docker-image
|
||||
icon: docker
|
||||
source:
|
||||
start: 8:00 AM
|
||||
stop: 9:00 AM
|
||||
location: Europe/Vienna
|
||||
<<: *docker-resource-source
|
||||
repository: ((docker-hub-organization))/spring-framework-ci-jdk11
|
||||
- name: artifactory-repo
|
||||
type: artifactory-resource
|
||||
icon: package-variant
|
||||
@@ -109,14 +104,6 @@ resources:
|
||||
username: ((artifactory-username))
|
||||
password: ((artifactory-password))
|
||||
build_name: ((build-name))
|
||||
- name: git-pull-request
|
||||
type: pull-request
|
||||
icon: source-pull
|
||||
source:
|
||||
access_token: ((github-ci-pull-request-token))
|
||||
repository: ((github-repo-name))
|
||||
base_branch: ((branch))
|
||||
ignore_paths: ["ci/*"]
|
||||
- name: repo-status-build
|
||||
type: github-status-resource
|
||||
icon: eye-check-outline
|
||||
@@ -125,22 +112,14 @@ resources:
|
||||
access_token: ((github-ci-status-token))
|
||||
branch: ((branch))
|
||||
context: build
|
||||
- name: repo-status-jdk18-build
|
||||
- name: repo-status-jdk11-build
|
||||
type: github-status-resource
|
||||
icon: eye-check-outline
|
||||
source:
|
||||
repository: ((github-repo-name))
|
||||
access_token: ((github-ci-status-token))
|
||||
branch: ((branch))
|
||||
context: jdk18-build
|
||||
- name: repo-status-jdk19-build
|
||||
type: github-status-resource
|
||||
icon: eye-check-outline
|
||||
source:
|
||||
repository: ((github-repo-name))
|
||||
access_token: ((github-ci-status-token))
|
||||
branch: ((branch))
|
||||
context: jdk19-build
|
||||
context: jdk11-build
|
||||
- name: slack-alert
|
||||
type: slack-notification
|
||||
icon: slack
|
||||
@@ -166,20 +145,17 @@ resources:
|
||||
jobs:
|
||||
- name: build-ci-images
|
||||
plan:
|
||||
- get: git-repo
|
||||
- get: ci-images-git-repo
|
||||
trigger: true
|
||||
- task: build-ci-image
|
||||
privileged: true
|
||||
file: git-repo/ci/tasks/build-ci-image.yml
|
||||
output_mapping:
|
||||
image: ci-image
|
||||
vars:
|
||||
ci-image-name: ci-image
|
||||
<<: *registry-mirror-vars
|
||||
- get: ci-images-git-repo
|
||||
trigger: true
|
||||
- in_parallel:
|
||||
- put: ci-image
|
||||
params:
|
||||
image: ci-image/image.tar
|
||||
build: ci-images-git-repo/ci/images
|
||||
dockerfile: ci-images-git-repo/ci/images/ci-image/Dockerfile
|
||||
- put: ci-image-jdk11
|
||||
params:
|
||||
build: ci-images-git-repo/ci/images
|
||||
dockerfile: ci-images-git-repo/ci/images/ci-image-jdk11/Dockerfile
|
||||
- name: build
|
||||
serial: true
|
||||
public: true
|
||||
@@ -193,10 +169,7 @@ jobs:
|
||||
- task: build-project
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/build-project.yml
|
||||
privileged: true
|
||||
timeout: ((task-timeout))
|
||||
params:
|
||||
<<: *build-project-task-params
|
||||
<<: *build-project-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-build
|
||||
@@ -218,112 +191,49 @@ jobs:
|
||||
threads: 8
|
||||
artifact_set:
|
||||
- include:
|
||||
- "/**/framework-docs-*.zip"
|
||||
- "/**/spring-*.zip"
|
||||
properties:
|
||||
"zip.name": "spring-framework"
|
||||
"zip.displayname": "Spring Framework"
|
||||
"zip.deployed": "false"
|
||||
- include:
|
||||
- "/**/framework-docs-*-docs.zip"
|
||||
- "/**/spring-*-docs.zip"
|
||||
properties:
|
||||
"zip.type": "docs"
|
||||
- include:
|
||||
- "/**/framework-docs-*-dist.zip"
|
||||
- "/**/spring-*-dist.zip"
|
||||
properties:
|
||||
"zip.type": "dist"
|
||||
- include:
|
||||
- "/**/framework-docs-*-schema.zip"
|
||||
- "/**/spring-*-schema.zip"
|
||||
properties:
|
||||
"zip.type": "schema"
|
||||
get_params:
|
||||
threads: 8
|
||||
- name: jdk18-build
|
||||
- name: jdk11-build
|
||||
serial: true
|
||||
public: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
- get: every-morning
|
||||
trigger: true
|
||||
- put: repo-status-jdk18-build
|
||||
params: { state: "pending", commit: "git-repo" }
|
||||
- do:
|
||||
- task: check-project
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/check-project.yml
|
||||
privileged: true
|
||||
timeout: ((task-timeout))
|
||||
params:
|
||||
TEST_TOOLCHAIN: 18
|
||||
<<: *build-project-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-jdk18-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk18-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- name: jdk19-build
|
||||
serial: true
|
||||
public: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
- get: every-morning
|
||||
trigger: true
|
||||
- put: repo-status-jdk19-build
|
||||
params: { state: "pending", commit: "git-repo" }
|
||||
- do:
|
||||
- task: check-project
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/check-project.yml
|
||||
privileged: true
|
||||
timeout: ((task-timeout))
|
||||
params:
|
||||
TEST_TOOLCHAIN: 19
|
||||
<<: *build-project-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-jdk19-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk19-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- name: build-pull-requests
|
||||
serial: true
|
||||
public: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
resource: git-pull-request
|
||||
trigger: true
|
||||
version: every
|
||||
- do:
|
||||
- put: git-pull-request
|
||||
- get: ci-image-jdk11
|
||||
- get: git-repo
|
||||
- get: every-morning
|
||||
trigger: true
|
||||
- put: repo-status-jdk11-build
|
||||
params: { state: "pending", commit: "git-repo" }
|
||||
- do:
|
||||
- task: check-project
|
||||
image: ci-image-jdk11
|
||||
file: git-repo/ci/tasks/check-project.yml
|
||||
<<: *build-project-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-jdk11-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
path: git-repo
|
||||
status: pending
|
||||
- task: build-pr
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/build-pr.yml
|
||||
privileged: true
|
||||
timeout: ((task-timeout))
|
||||
params:
|
||||
BRANCH: ((branch))
|
||||
on_success:
|
||||
put: git-pull-request
|
||||
params:
|
||||
path: git-repo
|
||||
status: success
|
||||
on_failure:
|
||||
put: git-pull-request
|
||||
params:
|
||||
path: git-repo
|
||||
status: failure
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk11-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- name: stage-milestone
|
||||
serial: true
|
||||
plan:
|
||||
@@ -356,6 +266,7 @@ jobs:
|
||||
download_artifacts: false
|
||||
save_build_info: true
|
||||
- task: promote
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/promote-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: M
|
||||
@@ -400,6 +311,7 @@ jobs:
|
||||
download_artifacts: false
|
||||
save_build_info: true
|
||||
- task: promote
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/promote-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RC
|
||||
@@ -444,6 +356,7 @@ jobs:
|
||||
download_artifacts: true
|
||||
save_build_info: true
|
||||
- task: promote
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/promote-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RELEASE
|
||||
@@ -471,10 +384,8 @@ jobs:
|
||||
|
||||
groups:
|
||||
- name: "builds"
|
||||
jobs: ["build", "jdk18-build", "jdk19-build"]
|
||||
jobs: ["build", "jdk11-build"]
|
||||
- name: "releases"
|
||||
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"]
|
||||
- name: "ci-images"
|
||||
jobs: ["build-ci-images"]
|
||||
- name: "pull-requests"
|
||||
jobs: [ "build-pull-requests" ]
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
source $(dirname $0)/common.sh
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check
|
||||
popd > /dev/null
|
||||
@@ -4,6 +4,5 @@ set -e
|
||||
source $(dirname $0)/common.sh
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK18 \
|
||||
-PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check
|
||||
popd > /dev/null
|
||||
|
||||
Regular → Executable
@@ -4,9 +4,14 @@ set -e
|
||||
CONFIG_DIR=git-repo/ci/config
|
||||
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
|
||||
|
||||
milestone=${version}
|
||||
if [[ $RELEASE_TYPE = "RELEASE" ]]; then
|
||||
milestone=${version%.RELEASE}
|
||||
fi
|
||||
|
||||
java -jar /github-changelog-generator.jar \
|
||||
--spring.config.location=${CONFIG_DIR}/changelog-generator.yml \
|
||||
${version} generated-changelog/changelog.md
|
||||
${milestone} generated-changelog/changelog.md
|
||||
|
||||
echo ${version} > generated-changelog/version
|
||||
echo v${version} > generated-changelog/tag
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
source $(dirname $0)/common.sh
|
||||
CONFIG_DIR=git-repo/ci/config
|
||||
|
||||
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
|
||||
export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json
|
||||
|
||||
java -jar /concourse-release-scripts.jar \
|
||||
java -jar /opt/concourse-release-scripts.jar \
|
||||
--spring.config.location=${CONFIG_DIR}/release-scripts.yml \
|
||||
publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; }
|
||||
|
||||
java -jar /concourse-release-scripts.jar \
|
||||
java -jar /opt/concourse-release-scripts.jar \
|
||||
--spring.config.location=${CONFIG_DIR}/release-scripts.yml \
|
||||
promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; }
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ elif [[ $RELEASE_TYPE = "RC" ]]; then
|
||||
stageVersion=$( get_next_rc_release $snapshotVersion)
|
||||
nextVersion=$snapshotVersion
|
||||
elif [[ $RELEASE_TYPE = "RELEASE" ]]; then
|
||||
stageVersion=$( get_next_release $snapshotVersion)
|
||||
stageVersion=$( get_next_release $snapshotVersion "RELEASE")
|
||||
nextVersion=$( bump_version_number $snapshotVersion)
|
||||
else
|
||||
echo "Unknown release type $RELEASE_TYPE" >&2; exit 1;
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json
|
||||
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
|
||||
java -jar /opt/concourse-release-scripts.jar syncToCentral "RELEASE" $BUILD_INFO_LOCATION || { exit 1; }
|
||||
|
||||
echo "Sync complete"
|
||||
echo $version > version/version
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
platform: linux
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
tag: 0.10.0
|
||||
registry_mirror:
|
||||
host: ((registry-mirror-host))
|
||||
username: ((registry-mirror-username))
|
||||
password: ((registry-mirror-password))
|
||||
inputs:
|
||||
- name: ci-images-git-repo
|
||||
outputs:
|
||||
- name: image
|
||||
caches:
|
||||
- path: ci-image-cache
|
||||
params:
|
||||
CONTEXT: ci-images-git-repo/ci/images
|
||||
DOCKERFILE: ci-images-git-repo/ci/images/ci-image/Dockerfile
|
||||
DOCKER_HUB_AUTH: ((docker-hub-auth))
|
||||
run:
|
||||
path: /bin/sh
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
mkdir -p /root/.docker
|
||||
cat > /root/.docker/config.json <<EOF
|
||||
{ "auths": { "https://index.docker.io/v1/": { "auth": "$DOCKER_HUB_AUTH" }}}
|
||||
EOF
|
||||
build
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
platform: linux
|
||||
inputs:
|
||||
- name: git-repo
|
||||
caches:
|
||||
- path: gradle
|
||||
params:
|
||||
BRANCH:
|
||||
CI: true
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME:
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD:
|
||||
GRADLE_ENTERPRISE_URL: https://ge.spring.io
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
${PWD}/git-repo/ci/scripts/build-pr.sh
|
||||
@@ -10,8 +10,6 @@ caches:
|
||||
params:
|
||||
BRANCH:
|
||||
CI: true
|
||||
MAIN_TOOLCHAIN:
|
||||
TEST_TOOLCHAIN:
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME:
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD:
|
||||
|
||||
Executable → Regular
+1
-1
@@ -4,7 +4,7 @@ image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: springio/github-changelog-generator
|
||||
tag: '0.0.8'
|
||||
tag: '0.0.7'
|
||||
inputs:
|
||||
- name: git-repo
|
||||
- name: artifactory-repo
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
---
|
||||
platform: linux
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: springio/concourse-release-scripts
|
||||
tag: '0.3.4'
|
||||
inputs:
|
||||
- name: git-repo
|
||||
- name: artifactory-repo
|
||||
@@ -18,6 +13,6 @@ params:
|
||||
SONATYPE_USER:
|
||||
SONATYPE_PASSWORD:
|
||||
SONATYPE_URL:
|
||||
SONATYPE_STAGING_PROFILE:
|
||||
SONATYPE_STAGING_PROFILE_ID:
|
||||
run:
|
||||
path: git-repo/ci/scripts/promote-version.sh
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
description = "Spring Framework (Bill of Materials)"
|
||||
|
||||
apply plugin: 'java-platform'
|
||||
apply from: "$rootDir/gradle/publications.gradle"
|
||||
|
||||
group = "org.springframework"
|
||||
|
||||
dependencies {
|
||||
constraints {
|
||||
parent.moduleProjects.sort { "$it.name" }.each {
|
||||
api it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
artifactId = 'spring-framework-bom'
|
||||
from components.javaPlatform
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
description = "Spring Framework Docs"
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'org.asciidoctor.jvm.convert'
|
||||
apply plugin: 'org.asciidoctor.jvm.pdf'
|
||||
apply from: "${rootDir}/gradle/publications.gradle"
|
||||
|
||||
|
||||
configurations {
|
||||
asciidoctorExtensions
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":spring-context"))
|
||||
api(project(":spring-web"))
|
||||
|
||||
implementation(project(":spring-core-test"))
|
||||
implementation("org.assertj:assertj-core")
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
sourceSets = []
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
javadoc {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
dependencies {
|
||||
asciidoctorExtensions "io.spring.asciidoctor.backends:spring-asciidoctor-backends:0.0.3"
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.spring.io/release"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce Javadoc for all Spring Framework modules in "build/docs/javadoc"
|
||||
*/
|
||||
task api(type: Javadoc) {
|
||||
group = "Documentation"
|
||||
description = "Generates aggregated Javadoc API documentation."
|
||||
title = "${rootProject.description} ${version} API"
|
||||
|
||||
dependsOn {
|
||||
moduleProjects.collect {
|
||||
it.tasks.getByName("jar")
|
||||
}
|
||||
}
|
||||
doFirst {
|
||||
classpath = files(
|
||||
// ensure the javadoc process can resolve types compiled from .aj sources
|
||||
project(":spring-aspects").sourceSets.main.output
|
||||
)
|
||||
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
|
||||
}
|
||||
|
||||
options {
|
||||
encoding = "UTF-8"
|
||||
memberLevel = JavadocMemberLevel.PROTECTED
|
||||
author = true
|
||||
header = rootProject.description
|
||||
use = true
|
||||
overview = "framework-docs/src/docs/api/overview.html"
|
||||
splitIndex = true
|
||||
links(project.ext.javadocLinks)
|
||||
addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint
|
||||
addBooleanOption('Werror', true) // fail build on Javadoc warnings
|
||||
}
|
||||
source moduleProjects.collect { project ->
|
||||
project.sourceSets.main.allJava
|
||||
}
|
||||
maxMemory = "1024m"
|
||||
destinationDir = file("$buildDir/docs/javadoc")
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce KDoc for all Spring Framework modules in "build/docs/kdoc"
|
||||
*/
|
||||
rootProject.tasks.dokkaHtmlMultiModule.configure {
|
||||
dependsOn {
|
||||
tasks.getByName("api")
|
||||
}
|
||||
moduleName.set("spring-framework")
|
||||
outputDirectory.set(project.file("$buildDir/docs/kdoc"))
|
||||
}
|
||||
|
||||
asciidoctorj {
|
||||
version = '2.4.3'
|
||||
fatalWarnings ".*"
|
||||
options doctype: 'book', eruby: 'erubis'
|
||||
attributes([
|
||||
icons: 'font',
|
||||
idprefix: '',
|
||||
idseparator: '-',
|
||||
revnumber: project.version,
|
||||
sectanchors: '',
|
||||
sectnums: '',
|
||||
'spring-version': project.version
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Spring Framework Reference documentation from
|
||||
* "src/docs/asciidoc" in "build/docs/ref-docs/html5".
|
||||
*/
|
||||
asciidoctor {
|
||||
baseDirFollowsSourceDir()
|
||||
configurations "asciidoctorExtensions"
|
||||
sources {
|
||||
include '*.adoc'
|
||||
}
|
||||
outputDir "$buildDir/docs/ref-docs/html5"
|
||||
outputOptions {
|
||||
backends "spring-html"
|
||||
}
|
||||
logDocuments = true
|
||||
resources {
|
||||
from(sourceDir) {
|
||||
include 'images/*.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Spring Framework Reference documentation from "src/docs/asciidoc"
|
||||
* in "build/docs/ref-docs/pdf".
|
||||
*/
|
||||
asciidoctorPdf {
|
||||
baseDirFollowsSourceDir()
|
||||
configurations 'asciidoctorExtensions'
|
||||
sources {
|
||||
include 'spring-framework.adocbook'
|
||||
}
|
||||
outputDir "$buildDir/docs/ref-docs/pdf"
|
||||
forkOptions {
|
||||
jvmArgs += ["--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED", "--add-opens", "java.base/java.io=ALL-UNNAMED"]
|
||||
}
|
||||
logDocuments = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip all docs (API and reference) into a single archive
|
||||
*/
|
||||
task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor', 'asciidoctorPdf', rootProject.tasks.dokkaHtmlMultiModule]) {
|
||||
group = "Distribution"
|
||||
description = "Builds -${archiveClassifier} archive containing api and reference " +
|
||||
"for deployment at https://docs.spring.io/spring-framework/docs/."
|
||||
|
||||
archiveBaseName.set("spring-framework")
|
||||
archiveClassifier.set("docs")
|
||||
from("src/dist") {
|
||||
include "changelog.txt"
|
||||
}
|
||||
from (api) {
|
||||
into "javadoc-api"
|
||||
}
|
||||
from ("$asciidoctor.outputDir") {
|
||||
into "reference/html"
|
||||
}
|
||||
from ("$asciidoctorPdf.outputDir") {
|
||||
into "reference/pdf"
|
||||
}
|
||||
from (rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) {
|
||||
into "kdoc-api"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip all Spring Framework schemas into a single archive
|
||||
*/
|
||||
task schemaZip(type: Zip) {
|
||||
group = "Distribution"
|
||||
archiveBaseName.set("spring-framework")
|
||||
archiveClassifier.set("schema")
|
||||
description = "Builds -${archiveClassifier} archive containing all " +
|
||||
"XSDs for deployment at https://springframework.org/schema."
|
||||
duplicatesStrategy DuplicatesStrategy.EXCLUDE
|
||||
moduleProjects.each { module ->
|
||||
def Properties schemas = new Properties();
|
||||
|
||||
module.sourceSets.main.resources.find {
|
||||
(it.path.endsWith("META-INF/spring.schemas") || it.path.endsWith("META-INF\\spring.schemas"))
|
||||
}?.withInputStream { schemas.load(it) }
|
||||
|
||||
for (def key : schemas.keySet()) {
|
||||
def shortName = key.replaceAll(/http.*schema.(.*).spring-.*/, '$1')
|
||||
assert shortName != key
|
||||
File xsdFile = module.sourceSets.main.resources.find {
|
||||
(it.path.endsWith(schemas.get(key)) || it.path.endsWith(schemas.get(key).replaceAll('\\/','\\\\')))
|
||||
}
|
||||
assert xsdFile != null
|
||||
into (shortName) {
|
||||
from xsdFile.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a distribution zip with everything:
|
||||
* docs, schemas, jars, source jars, javadoc jars
|
||||
*/
|
||||
task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) {
|
||||
group = "Distribution"
|
||||
archiveBaseName.set("spring-framework")
|
||||
archiveClassifier.set("dist")
|
||||
description = "Builds -${archiveClassifier} archive, containing all jars and docs, " +
|
||||
"suitable for community download page."
|
||||
|
||||
ext.baseDir = "spring-framework-${project.version}";
|
||||
|
||||
from("src/docs/dist") {
|
||||
include "readme.txt"
|
||||
include "license.txt"
|
||||
include "notice.txt"
|
||||
into "${baseDir}"
|
||||
expand(copyright: new Date().format("yyyy"), version: project.version)
|
||||
}
|
||||
|
||||
from(zipTree(docsZip.archiveFile)) {
|
||||
into "${baseDir}/docs"
|
||||
}
|
||||
|
||||
from(zipTree(schemaZip.archiveFile)) {
|
||||
into "${baseDir}/schema"
|
||||
}
|
||||
|
||||
moduleProjects.each { module ->
|
||||
into ("${baseDir}/libs") {
|
||||
from module.jar
|
||||
if (module.tasks.findByPath("sourcesJar")) {
|
||||
from module.sourcesJar
|
||||
}
|
||||
if (module.tasks.findByPath("javadocJar")) {
|
||||
from module.javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
distZip.mustRunAfter moduleProjects.check
|
||||
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
artifact docsZip
|
||||
artifact schemaZip
|
||||
artifact distZip
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
This is the public API documentation for the <a href="https://github.com/spring-projects/spring-framework" target="_top">Spring Framework</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,80 +0,0 @@
|
||||
[[appendix]]
|
||||
= Appendix
|
||||
include::attributes.adoc[]
|
||||
include::page-layout.adoc[]
|
||||
|
||||
This part of the reference documentation covers topics that apply to multiple modules
|
||||
within the core Spring Framework.
|
||||
|
||||
|
||||
[[appendix-spring-properties]]
|
||||
== Spring Properties
|
||||
|
||||
{api-spring-framework}/core/SpringProperties.html[`SpringProperties`] is a static holder
|
||||
for properties that control certain low-level aspects of the Spring Framework. Users can
|
||||
configure these properties via JVM system properties or programmatically via the
|
||||
`SpringProperties.setProperty(String key, String value)` method. The latter may be
|
||||
necessary if the deployment environment disallows custom JVM system properties. As an
|
||||
alternative, these properties may be configured in a `spring.properties` file in the root
|
||||
of the classpath -- for example, deployed within the application's JAR file.
|
||||
|
||||
The following table lists all currently supported Spring properties.
|
||||
|
||||
.Supported Spring Properties
|
||||
|===
|
||||
| Name | Description
|
||||
|
||||
| `spring.beaninfo.ignore`
|
||||
| Instructs Spring to use the `Introspector.IGNORE_ALL_BEANINFO` mode when calling the
|
||||
JavaBeans `Introspector`. See
|
||||
{api-spring-framework}++/beans/CachedIntrospectionResults.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`]
|
||||
for details.
|
||||
|
||||
| `spring.expression.compiler.mode`
|
||||
| The mode to use when compiling expressions for the
|
||||
<<core.adoc#expressions-compiler-configuration, Spring Expression Language>>.
|
||||
|
||||
| `spring.getenv.ignore`
|
||||
| Instructs Spring to ignore operating system environment variables if a Spring
|
||||
`Environment` property -- for example, a placeholder in a configuration String -- isn't
|
||||
resolvable otherwise. See
|
||||
{api-spring-framework}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`]
|
||||
for details.
|
||||
|
||||
| `spring.index.ignore`
|
||||
| Instructs Spring to ignore the components index located in
|
||||
`META-INF/spring.components`. See <<core.adoc#beans-scanning-index, Generating an Index
|
||||
of Candidate Components>>.
|
||||
|
||||
| `spring.jdbc.getParameterType.ignore`
|
||||
| Instructs Spring to ignore `java.sql.ParameterMetaData.getParameterType` completely.
|
||||
See the note in <<data-access.adoc#jdbc-batch-list, Batch Operations with a List of Objects>>.
|
||||
|
||||
| `spring.jndi.ignore`
|
||||
| Instructs Spring to ignore a default JNDI environment, as an optimization for scenarios
|
||||
where nothing is ever to be found for such JNDI fallback searches to begin with, avoiding
|
||||
the repeated JNDI lookup overhead. See
|
||||
{api-spring-framework}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`]
|
||||
for details.
|
||||
|
||||
| `spring.objenesis.ignore`
|
||||
| Instructs Spring to ignore Objenesis, not even attempting to use it. See
|
||||
{api-spring-framework}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
|
||||
for details.
|
||||
|
||||
| `spring.test.constructor.autowire.mode`
|
||||
| The default _test constructor autowire mode_ to use if `@TestConstructor` is not present
|
||||
on a test class. See <<testing.adoc#integration-testing-annotations-testconstructor,
|
||||
Changing the default test constructor autowire mode>>.
|
||||
|
||||
| `spring.test.context.cache.maxSize`
|
||||
| The maximum size of the context cache in the _Spring TestContext Framework_. See
|
||||
<<testing.adoc#testcontext-ctx-management-caching, Context Caching>>.
|
||||
|
||||
| `spring.test.enclosing.configuration`
|
||||
| The default _enclosing configuration inheritance mode_ to use if
|
||||
`@NestedTestConfiguration` is not present on a test class. See
|
||||
<<testing.adoc#integration-testing-annotations-nestedtestconfiguration, Changing the
|
||||
default enclosing configuration inheritance mode>>.
|
||||
|
||||
|===
|
||||
@@ -1,18 +0,0 @@
|
||||
:chomp: default headers packages
|
||||
:docs-site: https://docs.spring.io
|
||||
// Spring Framework
|
||||
:docs-spring-framework: {docs-site}/spring-framework/docs/{spring-version}
|
||||
:api-spring-framework: {docs-spring-framework}/javadoc-api/org/springframework
|
||||
:docs-java: {docdir}/../../main/java/org/springframework/docs
|
||||
:docs-kotlin: {docdir}/../../main/kotlin/org/springframework/docs
|
||||
:docs-resources: {docdir}/../../main/resources
|
||||
:spring-framework-main-code: https://github.com/spring-projects/spring-framework/tree/main
|
||||
// Spring portfolio Links
|
||||
:docs-spring-boot: {docs-site}/spring-boot/docs/current/reference
|
||||
:docs-spring-gemfire: {docs-site}/spring-gemfire/docs/current/reference
|
||||
:docs-spring-security: {docs-site}/spring-security/reference
|
||||
// Third-party Links
|
||||
:docs-graalvm: https://www.graalvm.org/22.3/reference-manual
|
||||
:gh-rsocket: https://github.com/rsocket
|
||||
:gh-rsocket-extensions: {gh-rsocket}/rsocket/blob/master/Extensions
|
||||
:gh-rsocket-java: {gh-rsocket}/rsocket-java
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,307 +0,0 @@
|
||||
[[core.aot]]
|
||||
= Ahead of Time Optimizations
|
||||
|
||||
This chapter covers Spring's Ahead of Time (AOT) optimizations.
|
||||
|
||||
For AOT support specific to integration tests, see <<testing.adoc#testcontext-aot, Ahead of Time Support for Tests>>.
|
||||
|
||||
[[core.aot.introduction]]
|
||||
== Introduction to Ahead of Time Optimizations
|
||||
|
||||
Spring's support for AOT optimizations is meant to inspect an `ApplicationContext` at build time and apply decisions and discovery logic that usually happens at runtime.
|
||||
Doing so allows building an application startup arrangement that is more straightforward and focused on a fixed set of features based mainly on the classpath and the `Environment`.
|
||||
|
||||
Applying such optimizations early implies the following restrictions:
|
||||
|
||||
* The classpath is fixed and fully defined at build time.
|
||||
* The beans defined in your application cannot change at runtime, meaning:
|
||||
** `@Profile`, in particular profile-specific configuration needs to be chosen at build time.
|
||||
** Environment properties that impact the presence of a bean (`@Conditional`) are only considered at build time.
|
||||
|
||||
When these restrictions are in place, it becomes possible to perform ahead-of-time processing at build time and generate additional assets.
|
||||
A Spring AOT processed application typically generates:
|
||||
|
||||
* Java source code
|
||||
* Bytecode (usually for dynamic proxies)
|
||||
* {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] for the use of reflection, resource loading, serialization, and JDK proxies.
|
||||
|
||||
NOTE: At the moment, AOT is focused on allowing Spring applications to be deployed as native images using GraalVM.
|
||||
We intend to support more JVM-based use cases in future generations.
|
||||
|
||||
[[core.aot.basics]]
|
||||
== AOT engine overview
|
||||
|
||||
The entry point of the AOT engine for processing an `ApplicationContext` arrangement is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {api-spring-framework}/aot/generate/GenerationContext.html[`GenerationContext`]:
|
||||
|
||||
* Refresh an `ApplicationContext` for AOT processing. Contrary to a traditional refresh, this version only creates bean definitions, not bean instances.
|
||||
* Invoke the available `BeanFactoryInitializationAotProcessor` implementations and apply their contributions against the `GenerationContext`.
|
||||
For instance, a core implementation iterates over all candidate bean definitions and generates the necessary code to restore the state of the `BeanFactory`.
|
||||
|
||||
Once this process completes, the `GenerationContext` will have been updated with the generated code, resources, and classes that are necessary for the application to run.
|
||||
The `RuntimeHints` instance can also be used to generate the relevant GraalVM native image configuration files.
|
||||
|
||||
`ApplicationContextAotGenerator#processAheadOfTime` returns the class name of the `ApplicationContextInitializer` entry point that allows the context to be started with AOT optimizations.
|
||||
|
||||
Those steps are covered in greater detail in the sections below.
|
||||
|
||||
[[core.aot.refresh]]
|
||||
== Refresh for AOT Processing
|
||||
|
||||
Refresh for AOT processing is supported on all `GenericApplicationContext` implementations.
|
||||
An application context is created with any number of entry points, usually in the form of `@Configuration`-annotated classes.
|
||||
|
||||
Let's look at a basic example:
|
||||
|
||||
include::code:AotProcessingSample[tag=myapplication]
|
||||
|
||||
Starting this application with the regular runtime involves a number of steps including classpath scanning, configuration class parsing, bean instantiation, and lifecycle callback handling.
|
||||
Refresh for AOT processing only applies a subset of what happens with a <<beans-introduction,regular `refresh`>>.
|
||||
AOT processing can be triggered as follows:
|
||||
|
||||
include::code:AotProcessingSample[tag=aotcontext]
|
||||
|
||||
In this mode, <<beans-factory-extension-factory-postprocessors,`BeanFactoryPostProcessor` implementations>> are invoked as usual.
|
||||
This includes configuration class parsing, import selectors, classpath scanning, etc.
|
||||
Such steps make sure that the `BeanRegistry` contains the relevant bean definitions for the application.
|
||||
If bean definitions are guarded by conditions (such as `@Profile`), these are discarded at this stage.
|
||||
|
||||
Because this mode does not actually create bean instances, `BeanPostProcessor` implementations are not invoked, except for specific variants that are relevant for AOT processing.
|
||||
These are:
|
||||
|
||||
* `MergedBeanDefinitionPostProcessor` implementations post-process bean definitions to extract additional settings, such as `init` and `destroy` methods.
|
||||
* `SmartInstantiationAwareBeanPostProcessor` implementations determine a more precise bean type if necessary.
|
||||
This makes sure to create any proxy that will be required at runtime.
|
||||
|
||||
One this part completes, the `BeanFactory` contains the bean definitions that are necessary for the application to run. It does not trigger bean instantiation but allows the AOT engine to inspect the beans that will be created at runtime.
|
||||
|
||||
[[core.aot.bean-factory-initialization-contributions]]
|
||||
== Bean Factory Initialization AOT Contributions
|
||||
|
||||
Components that want to participate in this step can implement the {api-spring-framework}/beans/factory/aot/BeanFactoryInitializationAotProcessor.html[`BeanFactoryInitializationAotProcessor`] interface.
|
||||
Each implementation can return an AOT contribution, based on the state of the bean factory.
|
||||
|
||||
An AOT contribution is a component that contributes generated code that reproduces a particular behavior.
|
||||
It can also contribute `RuntimeHints` to indicate the need for reflection, resource loading, serialization, or JDK proxies.
|
||||
|
||||
A `BeanFactoryInitializationAotProcessor` implementation can be registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface.
|
||||
|
||||
A `BeanFactoryInitializationAotProcessor` can also be implemented directly by a bean.
|
||||
In this mode, the bean provides an AOT contribution equivalent to the feature it provides with a regular runtime.
|
||||
Consequently, such a bean is automatically excluded from the AOT-optimized context.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
If a bean implements the `BeanFactoryInitializationAotProcessor` interface, the bean and **all** of its dependencies will be initialized during AOT processing.
|
||||
We generally recommend that this interface is only implemented by infrastructure beans such as `BeanFactoryPostProcessor` which have limited dependencies and are already initialized early in the bean factory lifecycle.
|
||||
If such a bean is registered using an `@Bean` factory method, ensure the method is `static` so that its enclosing `@Configuration` class does not have to be initialized.
|
||||
====
|
||||
|
||||
|
||||
[[core.aot.bean-registration-contributions]]
|
||||
=== Bean Registration AOT Contributions
|
||||
|
||||
A core `BeanFactoryInitializationAotProcessor` implementation is responsible for collecting the necessary contributions for each candidate `BeanDefinition`.
|
||||
It does so using a dedicated `BeanRegistrationAotProcessor`.
|
||||
|
||||
This interface is used as follows:
|
||||
|
||||
* Implemented by a `BeanPostProcessor` bean, to replace its runtime behavior.
|
||||
For instance <<beans-factory-extension-bpp-examples-aabpp,`AutowiredAnnotationBeanPostProcessor`>> implements this interface to generate code that injects members annotated with `@Autowired`.
|
||||
* Implemented by a type registered in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the interface.
|
||||
Typically used when the bean definition needs to be tuned for specific features of the core framework.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
If a bean implements the `BeanRegistrationAotProcessor` interface, the bean and **all** of its dependencies will be initialized during AOT processing.
|
||||
We generally recommend that this interface is only implemented by infrastructure beans such as `BeanFactoryPostProcessor` which have limited dependencies and are already initialized early in the bean factory lifecycle.
|
||||
If such a bean is registered using an `@Bean` factory method, ensure the method is `static` so that its enclosing `@Configuration` class does not have to be initialized.
|
||||
====
|
||||
|
||||
If no `BeanRegistrationAotProcessor` handles a particular registered bean, a default implementation processes it.
|
||||
This is the default behavior, since tuning the generated code for a bean definition should be restricted to corner cases.
|
||||
|
||||
Taking our previous example, let's assume that `DataSourceConfiguration` is as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class DataSourceConfiguration {
|
||||
|
||||
@Bean
|
||||
public SimpleDataSource dataSource() {
|
||||
return new SimpleDataSource();
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Since there isn't any particular condition on this class, `dataSourceConfiguration` and `dataSource` are identified as candidates.
|
||||
The AOT engine will convert the configuration class above to code similar to the following:
|
||||
|
||||
[source,java,indent=0,role="primary"]
|
||||
.Java
|
||||
----
|
||||
/**
|
||||
* Bean definitions for {@link DataSourceConfiguration}
|
||||
*/
|
||||
public class DataSourceConfiguration__BeanDefinitions {
|
||||
/**
|
||||
* Get the bean definition for 'dataSourceConfiguration'
|
||||
*/
|
||||
public static BeanDefinition getDataSourceConfigurationBeanDefinition() {
|
||||
Class<?> beanType = DataSourceConfiguration.class;
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
|
||||
beanDefinition.setInstanceSupplier(DataSourceConfiguration::new);
|
||||
return beanDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bean instance supplier for 'dataSource'.
|
||||
*/
|
||||
private static BeanInstanceSupplier<SimpleDataSource> getDataSourceInstanceSupplier() {
|
||||
return BeanInstanceSupplier.<SimpleDataSource>forFactoryMethod(DataSourceConfiguration.class, "dataSource")
|
||||
.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bean definition for 'dataSource'
|
||||
*/
|
||||
public static BeanDefinition getDataSourceBeanDefinition() {
|
||||
Class<?> beanType = SimpleDataSource.class;
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
|
||||
beanDefinition.setInstanceSupplier(getDataSourceInstanceSupplier());
|
||||
return beanDefinition;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: The exact code generated may differ depending on the exact nature of your bean definitions.
|
||||
|
||||
The generated code above creates bean definitions equivalent to the `@Configuration` class, but in a direct way and without the use of reflection if at all possible.
|
||||
There is a bean definition for `dataSourceConfiguration` and one for `dataSourceBean`.
|
||||
When a `datasource` instance is required, a `BeanInstanceSupplier` is called.
|
||||
This supplier invokes the `dataSource()` method on the `dataSourceConfiguration` bean.
|
||||
|
||||
|
||||
[[core.aot.hints]]
|
||||
== Runtime Hints
|
||||
|
||||
Running an application as a native image requires additional information compared to a regular JVM runtime.
|
||||
For instance, GraalVM needs to know ahead of time if a component uses reflection.
|
||||
Similarly, classpath resources are not shipped in a native image unless specified explicitly.
|
||||
Consequently, if the application needs to load a resource, it must be referenced from the corresponding GraalVM native image configuration file.
|
||||
|
||||
The {api-spring-framework}/aot/hint/RuntimeHints.html[`RuntimeHints`] API collects the need for reflection, resource loading, serialization, and JDK proxies at runtime.
|
||||
The following example makes sure that `config/app.properties` can be loaded from the classpath at runtime within a native image:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
runtimeHints.resources().registerPattern("config/app.properties");
|
||||
----
|
||||
|
||||
A number of contracts are handled automatically during AOT processing.
|
||||
For instance, the return type of a `@Controller` method is inspected, and relevant reflection hints are added if Spring detects that the type should be serialized (typically to JSON).
|
||||
|
||||
For cases that the core container cannot infer, you can register such hints programmatically.
|
||||
A number of convenient annotations are also provided for common use cases.
|
||||
|
||||
|
||||
[[core.aot.hints.import-runtime-hints]]
|
||||
=== `@ImportRuntimeHints`
|
||||
|
||||
`RuntimeHintsRegistrar` implementations allow you to get a callback to the `RuntimeHints` instance managed by the AOT engine.
|
||||
Implementations of this interface can be registered using `@ImportRuntimeHints` on any Spring bean or `@Bean` factory method.
|
||||
`RuntimeHintsRegistrar` implementations are detected and invoked at build time.
|
||||
|
||||
include::code:SpellCheckService[]
|
||||
|
||||
If at all possible, `@ImportRuntimeHints` should be used as close as possible to the component that requires the hints.
|
||||
This way, if the component is not contributed to the `BeanFactory`, the hints won't be contributed either.
|
||||
|
||||
It is also possible to register an implementation statically by adding an entry in `META-INF/spring/aot.factories` with a key equal to the fully qualified name of the `RuntimeHintsRegistrar` interface.
|
||||
|
||||
|
||||
[[core.aot.hints.reflective]]
|
||||
=== `@Reflective`
|
||||
|
||||
{api-spring-framework}/aot/hint/annotation/Reflective.html[`@Reflective`] provides an idiomatic way to flag the need for reflection on an annotated element.
|
||||
For instance, `@EventListener` is meta-annotated with `@Reflective` since the underlying implementation invokes the annotated method using reflection.
|
||||
|
||||
By default, only Spring beans are considered and an invocation hint is registered for the annotated element.
|
||||
This can be tuned by specifying a custom `ReflectiveProcessor` implementation via the
|
||||
`@Reflective` annotation.
|
||||
|
||||
Library authors can reuse this annotation for their own purposes.
|
||||
If components other than Spring beans need to be processed, a `BeanFactoryInitializationAotProcessor` can detect the relevant types and use `ReflectiveRuntimeHintsRegistrar` to process them.
|
||||
|
||||
|
||||
[[core.aot.hints.register-reflection-for-binding]]
|
||||
=== `@RegisterReflectionForBinding`
|
||||
|
||||
{api-spring-framework}/aot/hint/annotation/RegisterReflectionForBinding.html[`@RegisterReflectionForBinding`] is a specialization of `@Reflective` that registers the need for serializing arbitrary types.
|
||||
A typical use case is the use of DTOs that the container cannot infer, such as using a web client within a method body.
|
||||
|
||||
`@RegisterReflectionForBinding` can be applied to any Spring bean at the class level, but it can also be applied directly to a method, field, or constructor to better indicate where the hints are actually required.
|
||||
The following example registers `Account` for serialization.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Component
|
||||
public class OrderService {
|
||||
|
||||
@RegisterReflectionForBinding(Account.class)
|
||||
public void process(Order order) {
|
||||
// ...
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
[[core.aot.hints.testing]]
|
||||
=== Testing Runtime Hints
|
||||
|
||||
Spring Core also ships `RuntimeHintsPredicates`, a utility for checking that existing hints match a particular use case.
|
||||
This can be used in your own tests to validate that a `RuntimeHintsRegistrar` contains the expected results.
|
||||
We can write a test for our `SpellCheckService` and ensure that we will be able to load a dictionary at runtime:
|
||||
|
||||
include::code:SpellCheckServiceTests[tag=hintspredicates]
|
||||
|
||||
With `RuntimeHintsPredicates`, we can check for reflection, resource, serialization, or proxy generation hints.
|
||||
This approach works well for unit tests but implies that the runtime behavior of a component is well known.
|
||||
|
||||
You can learn more about the global runtime behavior of an application by running its test suite (or the app itself) with the {docs-graalvm}/native-image/metadata/AutomaticMetadataCollection/[GraalVM tracing agent].
|
||||
This agent will record all relevant calls requiring GraalVM hints at runtime and write them out as JSON configuration files.
|
||||
|
||||
For more targeted discovery and testing, Spring Framework ships a dedicated module with core AOT testing utilities, `"org.springframework:spring-core-test"`.
|
||||
This module contains the RuntimeHints Agent, a Java agent that records all method invocations that are related to runtime hints and helps you to assert that a given `RuntimeHints` instance covers all recorded invocations.
|
||||
Let's consider a piece of infrastructure for which we'd like to test the hints we're contributing during the AOT processing phase.
|
||||
|
||||
include::code:SampleReflection[]
|
||||
|
||||
We can then write a unit test (no native compilation required) that checks our contributed hints:
|
||||
|
||||
include::code:SampleReflectionRuntimeHintsTests[]
|
||||
|
||||
If you forgot to contribute a hint, the test will fail and provide some details about the invocation:
|
||||
|
||||
[source,txt,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
org.springframework.docs.core.aot.hints.testing.SampleReflection performReflection
|
||||
INFO: Spring version:6.0.0-SNAPSHOT
|
||||
|
||||
Missing <"ReflectionHints"> for invocation <java.lang.Class#forName>
|
||||
with arguments ["org.springframework.core.SpringVersion",
|
||||
false,
|
||||
jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7].
|
||||
Stacktrace:
|
||||
<"org.springframework.util.ClassUtils#forName, Line 284
|
||||
io.spring.runtimehintstesting.SampleReflection#performReflection, Line 19
|
||||
io.spring.runtimehintstesting.SampleReflectionRuntimeHintsTests#lambda$shouldRegisterReflectionHints$0, Line 25
|
||||
----
|
||||
|
||||
There are various ways to configure this Java agent in your build, so please refer to the documentation of your build tool and test execution plugin.
|
||||
The agent itself can be configured to instrument specific packages (by default, only `org.springframework` is instrumented).
|
||||
You'll find more details in the {spring-framework-main-code}/buildSrc/README.md[Spring Framework `buildSrc` README] file.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,965 +0,0 @@
|
||||
[[resources]]
|
||||
= Resources
|
||||
|
||||
This chapter covers how Spring handles resources and how you can work with resources in
|
||||
Spring. It includes the following topics:
|
||||
|
||||
* <<resources-introduction>>
|
||||
* <<resources-resource>>
|
||||
* <<resources-implementations>>
|
||||
* <<resources-resourceloader>>
|
||||
* <<resources-resourcepatternresolver>>
|
||||
* <<resources-resourceloaderaware>>
|
||||
* <<resources-as-dependencies>>
|
||||
* <<resources-app-ctx>>
|
||||
|
||||
|
||||
|
||||
|
||||
[[resources-introduction]]
|
||||
== Introduction
|
||||
|
||||
Java's standard `java.net.URL` class and standard handlers for various URL prefixes,
|
||||
unfortunately, are not quite adequate enough for all access to low-level resources. For
|
||||
example, there is no standardized `URL` implementation that may be used to access a
|
||||
resource that needs to be obtained from the classpath or relative to a
|
||||
`ServletContext`. While it is possible to register new handlers for specialized `URL`
|
||||
prefixes (similar to existing handlers for prefixes such as `http:`), this is generally
|
||||
quite complicated, and the `URL` interface still lacks some desirable functionality,
|
||||
such as a method to check for the existence of the resource being pointed to.
|
||||
|
||||
|
||||
|
||||
|
||||
[[resources-resource]]
|
||||
== The `Resource` Interface
|
||||
|
||||
Spring's `Resource` interface located in the `org.springframework.core.io.` package is
|
||||
meant to be a more capable interface for abstracting access to low-level resources. The
|
||||
following listing provides an overview of the `Resource` interface. See the
|
||||
{api-spring-framework}/core/io/Resource.html[`Resource`] javadoc for further details.
|
||||
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public interface Resource extends InputStreamSource {
|
||||
|
||||
boolean exists();
|
||||
|
||||
boolean isReadable();
|
||||
|
||||
boolean isOpen();
|
||||
|
||||
boolean isFile();
|
||||
|
||||
URL getURL() throws IOException;
|
||||
|
||||
URI getURI() throws IOException;
|
||||
|
||||
File getFile() throws IOException;
|
||||
|
||||
ReadableByteChannel readableChannel() throws IOException;
|
||||
|
||||
long contentLength() throws IOException;
|
||||
|
||||
long lastModified() throws IOException;
|
||||
|
||||
Resource createRelative(String relativePath) throws IOException;
|
||||
|
||||
String getFilename();
|
||||
|
||||
String getDescription();
|
||||
}
|
||||
----
|
||||
|
||||
As the definition of the `Resource` interface shows, it extends the `InputStreamSource`
|
||||
interface. The following listing shows the definition of the `InputStreamSource`
|
||||
interface:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public interface InputStreamSource {
|
||||
|
||||
InputStream getInputStream() throws IOException;
|
||||
}
|
||||
----
|
||||
|
||||
Some of the most important methods from the `Resource` interface are:
|
||||
|
||||
* `getInputStream()`: Locates and opens the resource, returning an `InputStream` for
|
||||
reading from the resource. It is expected that each invocation returns a fresh
|
||||
`InputStream`. It is the responsibility of the caller to close the stream.
|
||||
* `exists()`: Returns a `boolean` indicating whether this resource actually exists in
|
||||
physical form.
|
||||
* `isOpen()`: Returns a `boolean` indicating whether this resource represents a handle
|
||||
with an open stream. If `true`, the `InputStream` cannot be read multiple times and
|
||||
must be read once only and then closed to avoid resource leaks. Returns `false` for
|
||||
all usual resource implementations, with the exception of `InputStreamResource`.
|
||||
* `getDescription()`: Returns a description for this resource, to be used for error
|
||||
output when working with the resource. This is often the fully qualified file name or
|
||||
the actual URL of the resource.
|
||||
|
||||
Other methods let you obtain an actual `URL` or `File` object representing the
|
||||
resource (if the underlying implementation is compatible and supports that
|
||||
functionality).
|
||||
|
||||
Some implementations of the `Resource` interface also implement the extended
|
||||
{api-spring-framework}/core/io/WritableResource.html[`WritableResource`] interface
|
||||
for a resource that supports writing to it.
|
||||
|
||||
Spring itself uses the `Resource` abstraction extensively, as an argument type in
|
||||
many method signatures when a resource is needed. Other methods in some Spring APIs
|
||||
(such as the constructors to various `ApplicationContext` implementations) take a
|
||||
`String` which in unadorned or simple form is used to create a `Resource` appropriate to
|
||||
that context implementation or, via special prefixes on the `String` path, let the
|
||||
caller specify that a specific `Resource` implementation must be created and used.
|
||||
|
||||
While the `Resource` interface is used a lot with Spring and by Spring, it is actually
|
||||
very convenient to use as a general utility class by itself in your own code, for access
|
||||
to resources, even when your code does not know or care about any other parts of Spring.
|
||||
While this couples your code to Spring, it really only couples it to this small set of
|
||||
utility classes, which serves as a more capable replacement for `URL` and can be
|
||||
considered equivalent to any other library you would use for this purpose.
|
||||
|
||||
NOTE: The `Resource` abstraction does not replace functionality. It wraps it where
|
||||
possible. For example, a `UrlResource` wraps a URL and uses the wrapped `URL` to do its
|
||||
work.
|
||||
|
||||
|
||||
|
||||
|
||||
[[resources-implementations]]
|
||||
== Built-in `Resource` Implementations
|
||||
|
||||
Spring includes several built-in `Resource` implementations:
|
||||
|
||||
* <<resources-implementations-urlresource>>
|
||||
* <<resources-implementations-classpathresource>>
|
||||
* <<resources-implementations-filesystemresource>>
|
||||
* <<resources-implementations-pathresource>>
|
||||
* <<resources-implementations-servletcontextresource>>
|
||||
* <<resources-implementations-inputstreamresource>>
|
||||
* <<resources-implementations-bytearrayresource>>
|
||||
|
||||
For a complete list of `Resource` implementations available in Spring, consult the
|
||||
"All Known Implementing Classes" section of the
|
||||
{api-spring-framework}/core/io/Resource.html[`Resource`] javadoc.
|
||||
|
||||
|
||||
|
||||
[[resources-implementations-urlresource]]
|
||||
=== `UrlResource`
|
||||
|
||||
`UrlResource` wraps a `java.net.URL` and can be used to access any object that is
|
||||
normally accessible with a URL, such as files, an HTTPS target, an FTP target, and
|
||||
others. All URLs have a standardized `String` representation, such that appropriate
|
||||
standardized prefixes are used to indicate one URL type from another. This includes
|
||||
`file:` for accessing filesystem paths, `https:` for accessing resources through the
|
||||
HTTPS protocol, `ftp:` for accessing resources through FTP, and others.
|
||||
|
||||
A `UrlResource` is created by Java code by explicitly using the `UrlResource` constructor
|
||||
but is often created implicitly when you call an API method that takes a `String`
|
||||
argument meant to represent a path. For the latter case, a JavaBeans `PropertyEditor`
|
||||
ultimately decides which type of `Resource` to create. If the path string contains a
|
||||
well-known (to property editor, that is) prefix (such as `classpath:`), it creates an
|
||||
appropriate specialized `Resource` for that prefix. However, if it does not recognize the
|
||||
prefix, it assumes the string is a standard URL string and creates a `UrlResource`.
|
||||
|
||||
|
||||
|
||||
[[resources-implementations-classpathresource]]
|
||||
=== `ClassPathResource`
|
||||
|
||||
This class represents a resource that should be obtained from the classpath. It uses
|
||||
either the thread context class loader, a given class loader, or a given class for
|
||||
loading resources.
|
||||
|
||||
This `Resource` implementation supports resolution as a `java.io.File` if the class path
|
||||
resource resides in the file system but not for classpath resources that reside in a
|
||||
jar and have not been expanded (by the servlet engine or whatever the environment is)
|
||||
to the filesystem. To address this, the various `Resource` implementations always support
|
||||
resolution as a `java.net.URL`.
|
||||
|
||||
A `ClassPathResource` is created by Java code by explicitly using the `ClassPathResource`
|
||||
constructor but is often created implicitly when you call an API method that takes a
|
||||
`String` argument meant to represent a path. For the latter case, a JavaBeans
|
||||
`PropertyEditor` recognizes the special prefix, `classpath:`, on the string path and
|
||||
creates a `ClassPathResource` in that case.
|
||||
|
||||
|
||||
|
||||
[[resources-implementations-filesystemresource]]
|
||||
=== `FileSystemResource`
|
||||
|
||||
This is a `Resource` implementation for `java.io.File` handles. It also supports
|
||||
`java.nio.file.Path` handles, applying Spring's standard String-based path
|
||||
transformations but performing all operations via the `java.nio.file.Files` API. For pure
|
||||
`java.nio.path.Path` based support use a `PathResource` instead. `FileSystemResource`
|
||||
supports resolution as a `File` and as a `URL`.
|
||||
|
||||
|
||||
|
||||
[[resources-implementations-pathresource]]
|
||||
=== `PathResource`
|
||||
|
||||
This is a `Resource` implementation for `java.nio.file.Path` handles, performing all
|
||||
operations and transformations via the `Path` API. It supports resolution as a `File` and
|
||||
as a `URL` and also implements the extended `WritableResource` interface. `PathResource`
|
||||
is effectively a pure `java.nio.path.Path` based alternative to `FileSystemResource` with
|
||||
different `createRelative` behavior.
|
||||
|
||||
|
||||
|
||||
[[resources-implementations-servletcontextresource]]
|
||||
=== `ServletContextResource`
|
||||
|
||||
This is a `Resource` implementation for `ServletContext` resources that interprets
|
||||
relative paths within the relevant web application's root directory.
|
||||
|
||||
It always supports stream access and URL access but allows `java.io.File` access only
|
||||
when the web application archive is expanded and the resource is physically on the
|
||||
filesystem. Whether or not it is expanded and on the filesystem or accessed
|
||||
directly from the JAR or somewhere else like a database (which is conceivable) is actually
|
||||
dependent on the Servlet container.
|
||||
|
||||
|
||||
|
||||
[[resources-implementations-inputstreamresource]]
|
||||
=== `InputStreamResource`
|
||||
|
||||
An `InputStreamResource` is a `Resource` implementation for a given `InputStream`. It
|
||||
should be used only if no specific `Resource` implementation is applicable. In
|
||||
particular, prefer `ByteArrayResource` or any of the file-based `Resource`
|
||||
implementations where possible.
|
||||
|
||||
In contrast to other `Resource` implementations, this is a descriptor for an
|
||||
already-opened resource. Therefore, it returns `true` from `isOpen()`. Do not use it if
|
||||
you need to keep the resource descriptor somewhere or if you need to read a stream
|
||||
multiple times.
|
||||
|
||||
|
||||
|
||||
[[resources-implementations-bytearrayresource]]
|
||||
=== `ByteArrayResource`
|
||||
|
||||
This is a `Resource` implementation for a given byte array. It creates a
|
||||
`ByteArrayInputStream` for the given byte array.
|
||||
|
||||
It is useful for loading content from any given byte array without having to resort to a
|
||||
single-use `InputStreamResource`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[resources-resourceloader]]
|
||||
== The `ResourceLoader` Interface
|
||||
|
||||
The `ResourceLoader` interface is meant to be implemented by objects that can return
|
||||
(that is, load) `Resource` instances. The following listing shows the `ResourceLoader`
|
||||
interface definition:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public interface ResourceLoader {
|
||||
|
||||
Resource getResource(String location);
|
||||
|
||||
ClassLoader getClassLoader();
|
||||
}
|
||||
----
|
||||
|
||||
All application contexts implement the `ResourceLoader` interface. Therefore, all
|
||||
application contexts may be used to obtain `Resource` instances.
|
||||
|
||||
When you call `getResource()` on a specific application context, and the location path
|
||||
specified doesn't have a specific prefix, you get back a `Resource` type that is
|
||||
appropriate to that particular application context. For example, assume the following
|
||||
snippet of code was run against a `ClassPathXmlApplicationContext` instance:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val template = ctx.getResource("some/resource/path/myTemplate.txt")
|
||||
----
|
||||
|
||||
Against a `ClassPathXmlApplicationContext`, that code returns a `ClassPathResource`. If
|
||||
the same method were run against a `FileSystemXmlApplicationContext` instance, it would
|
||||
return a `FileSystemResource`. For a `WebApplicationContext`, it would return a
|
||||
`ServletContextResource`. It would similarly return appropriate objects for each context.
|
||||
|
||||
As a result, you can load resources in a fashion appropriate to the particular application
|
||||
context.
|
||||
|
||||
On the other hand, you may also force `ClassPathResource` to be used, regardless of the
|
||||
application context type, by specifying the special `classpath:` prefix, as the following
|
||||
example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt")
|
||||
----
|
||||
|
||||
Similarly, you can force a `UrlResource` to be used by specifying any of the standard
|
||||
`java.net.URL` prefixes. The following examples use the `file` and `https` prefixes:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val template = ctx.getResource("file:///some/resource/path/myTemplate.txt")
|
||||
----
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt")
|
||||
----
|
||||
|
||||
The following table summarizes the strategy for converting `String` objects to `Resource`
|
||||
objects:
|
||||
|
||||
[[resources-resource-strings]]
|
||||
.Resource strings
|
||||
|===
|
||||
| Prefix| Example| Explanation
|
||||
|
||||
| classpath:
|
||||
| `classpath:com/myapp/config.xml`
|
||||
| Loaded from the classpath.
|
||||
|
||||
| file:
|
||||
| `\file:///data/config.xml`
|
||||
| Loaded as a `URL` from the filesystem. See also <<resources-filesystemresource-caveats>>.
|
||||
|
||||
| https:
|
||||
| `\https://myserver/logo.png`
|
||||
| Loaded as a `URL`.
|
||||
|
||||
| (none)
|
||||
| `/data/config.xml`
|
||||
| Depends on the underlying `ApplicationContext`.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
|
||||
[[resources-resourcepatternresolver]]
|
||||
== The `ResourcePatternResolver` Interface
|
||||
|
||||
The `ResourcePatternResolver` interface is an extension to the `ResourceLoader` interface
|
||||
which defines a strategy for resolving a location pattern (for example, an Ant-style path
|
||||
pattern) into `Resource` objects.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public interface ResourcePatternResolver extends ResourceLoader {
|
||||
|
||||
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
|
||||
|
||||
Resource[] getResources(String locationPattern) throws IOException;
|
||||
}
|
||||
----
|
||||
|
||||
As can be seen above, this interface also defines a special `classpath*:` resource prefix
|
||||
for all matching resources from the class path. Note that the resource location is
|
||||
expected to be a path without placeholders in this case -- for example,
|
||||
`classpath*:/config/beans.xml`. JAR files or different directories in the class path can
|
||||
contain multiple files with the same path and the same name. See
|
||||
<<resources-app-ctx-wildcards-in-resource-paths>> and its subsections for further details
|
||||
on wildcard support with the `classpath*:` resource prefix.
|
||||
|
||||
A passed-in `ResourceLoader` (for example, one supplied via
|
||||
<<resources-resourceloaderaware,`ResourceLoaderAware`>> semantics) can be checked whether
|
||||
it implements this extended interface too.
|
||||
|
||||
`PathMatchingResourcePatternResolver` is a standalone implementation that is usable
|
||||
outside an `ApplicationContext` and is also used by `ResourceArrayPropertyEditor` for
|
||||
populating `Resource[]` bean properties. `PathMatchingResourcePatternResolver` is able to
|
||||
resolve a specified resource location path into one or more matching `Resource` objects.
|
||||
The source path may be a simple path which has a one-to-one mapping to a target
|
||||
`Resource`, or alternatively may contain the special `classpath*:` prefix and/or internal
|
||||
Ant-style regular expressions (matched using Spring's
|
||||
`org.springframework.util.AntPathMatcher` utility). Both of the latter are effectively
|
||||
wildcards.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The default `ResourceLoader` in any standard `ApplicationContext` is in fact an instance
|
||||
of `PathMatchingResourcePatternResolver` which implements the `ResourcePatternResolver`
|
||||
interface. The same is true for the `ApplicationContext` instance itself which also
|
||||
implements the `ResourcePatternResolver` interface and delegates to the default
|
||||
`PathMatchingResourcePatternResolver`.
|
||||
====
|
||||
|
||||
|
||||
|
||||
|
||||
[[resources-resourceloaderaware]]
|
||||
== The `ResourceLoaderAware` Interface
|
||||
|
||||
The `ResourceLoaderAware` interface is a special callback interface which identifies
|
||||
components that expect to be provided a `ResourceLoader` reference. The following listing
|
||||
shows the definition of the `ResourceLoaderAware` interface:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public interface ResourceLoaderAware {
|
||||
|
||||
void setResourceLoader(ResourceLoader resourceLoader);
|
||||
}
|
||||
----
|
||||
|
||||
When a class implements `ResourceLoaderAware` and is deployed into an application context
|
||||
(as a Spring-managed bean), it is recognized as `ResourceLoaderAware` by the application
|
||||
context. The application context then invokes `setResourceLoader(ResourceLoader)`,
|
||||
supplying itself as the argument (remember, all application contexts in Spring implement
|
||||
the `ResourceLoader` interface).
|
||||
|
||||
Since an `ApplicationContext` is a `ResourceLoader`, the bean could also implement the
|
||||
`ApplicationContextAware` interface and use the supplied application context directly to
|
||||
load resources. However, in general, it is better to use the specialized `ResourceLoader`
|
||||
interface if that is all you need. The code would be coupled only to the resource loading
|
||||
interface (which can be considered a utility interface) and not to the whole Spring
|
||||
`ApplicationContext` interface.
|
||||
|
||||
In application components, you may also rely upon autowiring of the `ResourceLoader` as
|
||||
an alternative to implementing the `ResourceLoaderAware` interface. The _traditional_
|
||||
`constructor` and `byType` autowiring modes (as described in <<beans-factory-autowire>>)
|
||||
are capable of providing a `ResourceLoader` for either a constructor argument or a
|
||||
setter method parameter, respectively. For more flexibility (including the ability to
|
||||
autowire fields and multiple parameter methods), consider using the annotation-based
|
||||
autowiring features. In that case, the `ResourceLoader` is autowired into a field,
|
||||
constructor argument, or method parameter that expects the `ResourceLoader` type as long
|
||||
as the field, constructor, or method in question carries the `@Autowired` annotation.
|
||||
For more information, see <<beans-autowired-annotation>>.
|
||||
|
||||
NOTE: To load one or more `Resource` objects for a resource path that contains wildcards
|
||||
or makes use of the special `classpath*:` resource prefix, consider having an instance of
|
||||
<<resources-resourcepatternresolver,`ResourcePatternResolver`>> autowired into your
|
||||
application components instead of `ResourceLoader`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[resources-as-dependencies]]
|
||||
== Resources as Dependencies
|
||||
|
||||
If the bean itself is going to determine and supply the resource path through some sort
|
||||
of dynamic process, it probably makes sense for the bean to use the `ResourceLoader` or
|
||||
`ResourcePatternResolver` interface to load resources. For example, consider the loading
|
||||
of a template of some sort, where the specific resource that is needed depends on the
|
||||
role of the user. If the resources are static, it makes sense to eliminate the use of the
|
||||
`ResourceLoader` interface (or `ResourcePatternResolver` interface) completely, have the
|
||||
bean expose the `Resource` properties it needs, and expect them to be injected into it.
|
||||
|
||||
What makes it trivial to then inject these properties is that all application contexts
|
||||
register and use a special JavaBeans `PropertyEditor`, which can convert `String` paths
|
||||
to `Resource` objects. For example, the following `MyBean` class has a `template`
|
||||
property of type `Resource`.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
package example;
|
||||
|
||||
public class MyBean {
|
||||
|
||||
private Resource template;
|
||||
|
||||
public setTemplate(Resource template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class MyBean(var template: Resource)
|
||||
----
|
||||
|
||||
In an XML configuration file, the `template` property can be configured with a simple
|
||||
string for that resource, as the following example shows:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<bean id="myBean" class="example.MyBean">
|
||||
<property name="template" value="some/resource/path/myTemplate.txt"/>
|
||||
</bean>
|
||||
----
|
||||
|
||||
Note that the resource path has no prefix. Consequently, because the application context
|
||||
itself is going to be used as the `ResourceLoader`, the resource is loaded through a
|
||||
`ClassPathResource`, a `FileSystemResource`, or a `ServletContextResource`, depending on
|
||||
the exact type of the application context.
|
||||
|
||||
If you need to force a specific `Resource` type to be used, you can use a prefix. The
|
||||
following two examples show how to force a `ClassPathResource` and a `UrlResource` (the
|
||||
latter being used to access a file in the filesystem):
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<property name="template" value="classpath:some/resource/path/myTemplate.txt">
|
||||
----
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>
|
||||
----
|
||||
|
||||
If the `MyBean` class is refactored for use with annotation-driven configuration, the
|
||||
path to `myTemplate.txt` can be stored under a key named `template.path` -- for example,
|
||||
in a properties file made available to the Spring `Environment` (see
|
||||
<<beans-environment>>). The template path can then be referenced via the `@Value`
|
||||
annotation using a property placeholder (see <<beans-value-annotations>>). Spring will
|
||||
retrieve the value of the template path as a string, and a special `PropertyEditor` will
|
||||
convert the string to a `Resource` object to be injected into the `MyBean` constructor.
|
||||
The following example demonstrates how to achieve this.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Component
|
||||
public class MyBean {
|
||||
|
||||
private final Resource template;
|
||||
|
||||
public MyBean(@Value("${template.path}") Resource template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Component
|
||||
class MyBean(@Value("\${template.path}") private val template: Resource)
|
||||
----
|
||||
|
||||
If we want to support multiple templates discovered under the same path in multiple
|
||||
locations in the classpath -- for example, in multiple jars in the classpath -- we can
|
||||
use the special `classpath*:` prefix and wildcarding to define a `templates.path` key as
|
||||
`classpath*:/config/templates/*.txt`. If we redefine the `MyBean` class as follows,
|
||||
Spring will convert the template path pattern into an array of `Resource` objects that
|
||||
can be injected into the `MyBean` constructor.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Component
|
||||
public class MyBean {
|
||||
|
||||
private final Resource[] templates;
|
||||
|
||||
public MyBean(@Value("${templates.path}") Resource[] templates) {
|
||||
this.templates = templates;
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Component
|
||||
class MyBean(@Value("\${templates.path}") private val templates: Resource[])
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[resources-app-ctx]]
|
||||
== Application Contexts and Resource Paths
|
||||
|
||||
This section covers how to create application contexts with resources, including shortcuts
|
||||
that work with XML, how to use wildcards, and other details.
|
||||
|
||||
|
||||
|
||||
[[resources-app-ctx-construction]]
|
||||
=== Constructing Application Contexts
|
||||
|
||||
An application context constructor (for a specific application context type) generally
|
||||
takes a string or array of strings as the location paths of the resources, such as
|
||||
XML files that make up the definition of the context.
|
||||
|
||||
When such a location path does not have a prefix, the specific `Resource` type built from
|
||||
that path and used to load the bean definitions depends on and is appropriate to the
|
||||
specific application context. For example, consider the following example, which creates a
|
||||
`ClassPathXmlApplicationContext`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val ctx = ClassPathXmlApplicationContext("conf/appContext.xml")
|
||||
----
|
||||
|
||||
The bean definitions are loaded from the classpath, because a `ClassPathResource` is
|
||||
used. However, consider the following example, which creates a `FileSystemXmlApplicationContext`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ApplicationContext ctx =
|
||||
new FileSystemXmlApplicationContext("conf/appContext.xml");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val ctx = FileSystemXmlApplicationContext("conf/appContext.xml")
|
||||
----
|
||||
|
||||
Now the bean definitions are loaded from a filesystem location (in this case, relative to
|
||||
the current working directory).
|
||||
|
||||
Note that the use of the special `classpath` prefix or a standard URL prefix on the
|
||||
location path overrides the default type of `Resource` created to load the bean
|
||||
definitions. Consider the following example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ApplicationContext ctx =
|
||||
new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml")
|
||||
----
|
||||
|
||||
Using `FileSystemXmlApplicationContext` loads the bean definitions from the classpath.
|
||||
However, it is still a `FileSystemXmlApplicationContext`. If it is subsequently used as a
|
||||
`ResourceLoader`, any unprefixed paths are still treated as filesystem paths.
|
||||
|
||||
|
||||
[[resources-app-ctx-classpathxml]]
|
||||
==== Constructing `ClassPathXmlApplicationContext` Instances -- Shortcuts
|
||||
|
||||
The `ClassPathXmlApplicationContext` exposes a number of constructors to enable
|
||||
convenient instantiation. The basic idea is that you can supply merely a string array
|
||||
that contains only the filenames of the XML files themselves (without the leading path
|
||||
information) and also supply a `Class`. The `ClassPathXmlApplicationContext` then derives
|
||||
the path information from the supplied class.
|
||||
|
||||
Consider the following directory layout:
|
||||
|
||||
[literal,subs="verbatim,quotes"]
|
||||
----
|
||||
com/
|
||||
example/
|
||||
services.xml
|
||||
repositories.xml
|
||||
MessengerService.class
|
||||
----
|
||||
|
||||
The following example shows how a `ClassPathXmlApplicationContext` instance composed of
|
||||
the beans defined in files named `services.xml` and `repositories.xml` (which are on the
|
||||
classpath) can be instantiated:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ApplicationContext ctx = new ClassPathXmlApplicationContext(
|
||||
new String[] {"services.xml", "repositories.xml"}, MessengerService.class);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "repositories.xml"), MessengerService::class.java)
|
||||
----
|
||||
|
||||
See the {api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`]
|
||||
javadoc for details on the various constructors.
|
||||
|
||||
|
||||
|
||||
[[resources-app-ctx-wildcards-in-resource-paths]]
|
||||
=== Wildcards in Application Context Constructor Resource Paths
|
||||
|
||||
The resource paths in application context constructor values may be simple paths (as
|
||||
shown earlier), each of which has a one-to-one mapping to a target `Resource` or,
|
||||
alternately, may contain the special `classpath*:` prefix or internal Ant-style patterns
|
||||
(matched by using Spring's `PathMatcher` utility). Both of the latter are effectively
|
||||
wildcards.
|
||||
|
||||
One use for this mechanism is when you need to do component-style application assembly. All
|
||||
components can _publish_ context definition fragments to a well-known location path, and,
|
||||
when the final application context is created using the same path prefixed with
|
||||
`classpath*:`, all component fragments are automatically picked up.
|
||||
|
||||
Note that this wildcarding is specific to the use of resource paths in application context
|
||||
constructors (or when you use the `PathMatcher` utility class hierarchy directly) and is
|
||||
resolved at construction time. It has nothing to do with the `Resource` type itself.
|
||||
You cannot use the `classpath*:` prefix to construct an actual `Resource`, as
|
||||
a resource points to just one resource at a time.
|
||||
|
||||
|
||||
[[resources-app-ctx-ant-patterns-in-paths]]
|
||||
==== Ant-style Patterns
|
||||
|
||||
Path locations can contain Ant-style patterns, as the following example shows:
|
||||
|
||||
[literal,subs="verbatim,quotes"]
|
||||
----
|
||||
/WEB-INF/\*-context.xml
|
||||
com/mycompany/\**/applicationContext.xml
|
||||
file:C:/some/path/\*-context.xml
|
||||
classpath:com/mycompany/**/applicationContext.xml
|
||||
----
|
||||
|
||||
When the path location contains an Ant-style pattern, the resolver follows a more complex
|
||||
procedure to try to resolve the wildcard. It produces a `Resource` for the path up to the
|
||||
last non-wildcard segment and obtains a URL from it. If this URL is not a `jar:` URL or
|
||||
container-specific variant (such as `zip:` in WebLogic, `wsjar` in WebSphere, and so on),
|
||||
a `java.io.File` is obtained from it and used to resolve the wildcard by traversing the
|
||||
filesystem. In the case of a jar URL, the resolver either gets a
|
||||
`java.net.JarURLConnection` from it or manually parses the jar URL and then traverses the
|
||||
contents of the jar file to resolve the wildcards.
|
||||
|
||||
[[resources-app-ctx-portability]]
|
||||
===== Implications on Portability
|
||||
|
||||
If the specified path is already a `file` URL (either implicitly because the base
|
||||
`ResourceLoader` is a filesystem one or explicitly), wildcarding is guaranteed to
|
||||
work in a completely portable fashion.
|
||||
|
||||
If the specified path is a `classpath` location, the resolver must obtain the last
|
||||
non-wildcard path segment URL by making a `Classloader.getResource()` call. Since this
|
||||
is just a node of the path (not the file at the end), it is actually undefined (in the
|
||||
`ClassLoader` javadoc) exactly what sort of a URL is returned in this case. In practice,
|
||||
it is always a `java.io.File` representing the directory (where the classpath resource
|
||||
resolves to a filesystem location) or a jar URL of some sort (where the classpath resource
|
||||
resolves to a jar location). Still, there is a portability concern on this operation.
|
||||
|
||||
If a jar URL is obtained for the last non-wildcard segment, the resolver must be able to
|
||||
get a `java.net.JarURLConnection` from it or manually parse the jar URL, to be able to
|
||||
walk the contents of the jar and resolve the wildcard. This does work in most environments
|
||||
but fails in others, and we strongly recommend that the wildcard resolution of resources
|
||||
coming from jars be thoroughly tested in your specific environment before you rely on it.
|
||||
|
||||
|
||||
[[resources-classpath-wildcards]]
|
||||
==== The `classpath*:` Prefix
|
||||
|
||||
When constructing an XML-based application context, a location string may use the
|
||||
special `classpath*:` prefix, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ApplicationContext ctx =
|
||||
new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val ctx = ClassPathXmlApplicationContext("classpath*:conf/appContext.xml")
|
||||
----
|
||||
|
||||
This special prefix specifies that all classpath resources that match the given name
|
||||
must be obtained (internally, this essentially happens through a call to
|
||||
`ClassLoader.getResources(...)`) and then merged to form the final application
|
||||
context definition.
|
||||
|
||||
NOTE: The wildcard classpath relies on the `getResources()` method of the underlying
|
||||
`ClassLoader`. As most application servers nowadays supply their own `ClassLoader`
|
||||
implementation, the behavior might differ, especially when dealing with jar files. A
|
||||
simple test to check if `classpath*` works is to use the `ClassLoader` to load a file from
|
||||
within a jar on the classpath:
|
||||
`getClass().getClassLoader().getResources("<someFileInsideTheJar>")`. Try this test with
|
||||
files that have the same name but reside in two different locations -- for example, files
|
||||
with the same name and same path but in different jars on the classpath. In case an
|
||||
inappropriate result is returned, check the application server documentation for settings
|
||||
that might affect the `ClassLoader` behavior.
|
||||
|
||||
You can also combine the `classpath*:` prefix with a `PathMatcher` pattern in the
|
||||
rest of the location path (for example, `classpath*:META-INF/*-beans.xml`). In this
|
||||
case, the resolution strategy is fairly simple: A `ClassLoader.getResources()` call is
|
||||
used on the last non-wildcard path segment to get all the matching resources in the
|
||||
class loader hierarchy and then, off each resource, the same `PathMatcher` resolution
|
||||
strategy described earlier is used for the wildcard subpath.
|
||||
|
||||
|
||||
[[resources-wildcards-in-path-other-stuff]]
|
||||
==== Other Notes Relating to Wildcards
|
||||
|
||||
Note that `classpath*:`, when combined with Ant-style patterns, only works
|
||||
reliably with at least one root directory before the pattern starts, unless the actual
|
||||
target files reside in the file system. This means that a pattern such as
|
||||
`classpath*:*.xml` might not retrieve files from the root of jar files but rather only
|
||||
from the root of expanded directories.
|
||||
|
||||
Spring's ability to retrieve classpath entries originates from the JDK's
|
||||
`ClassLoader.getResources()` method, which only returns file system locations for an
|
||||
empty string (indicating potential roots to search). Spring evaluates
|
||||
`URLClassLoader` runtime configuration and the `java.class.path` manifest in jar files
|
||||
as well, but this is not guaranteed to lead to portable behavior.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The scanning of classpath packages requires the presence of corresponding directory
|
||||
entries in the classpath. When you build JARs with Ant, do not activate the `files-only`
|
||||
switch of the JAR task. Also, classpath directories may not get exposed based on security
|
||||
policies in some environments -- for example, stand-alone applications on JDK 1.7.0_45
|
||||
and higher (which requires 'Trusted-Library' to be set up in your manifests. See
|
||||
https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources).
|
||||
|
||||
On JDK 9's module path (Jigsaw), Spring's classpath scanning generally works as expected.
|
||||
Putting resources into a dedicated directory is highly recommendable here as well,
|
||||
avoiding the aforementioned portability problems with searching the jar file root level.
|
||||
====
|
||||
|
||||
Ant-style patterns with `classpath:` resources are not guaranteed to find matching
|
||||
resources if the root package to search is available in multiple classpath locations.
|
||||
Consider the following example of a resource location:
|
||||
|
||||
[literal,subs="verbatim,quotes"]
|
||||
----
|
||||
com/mycompany/package1/service-context.xml
|
||||
----
|
||||
|
||||
Now consider an Ant-style path that someone might use to try to find that file:
|
||||
|
||||
[literal,subs="verbatim,quotes"]
|
||||
----
|
||||
classpath:com/mycompany/**/service-context.xml
|
||||
----
|
||||
|
||||
Such a resource may exist in only one location in the classpath, but when a path such as
|
||||
the preceding example is used to try to resolve it, the resolver works off the (first)
|
||||
URL returned by `getResource("com/mycompany");`. If this base package node exists in
|
||||
multiple `ClassLoader` locations, the desired resource may not exist in the first
|
||||
location found. Therefore, in such cases you should prefer using `classpath*:` with the
|
||||
same Ant-style pattern, which searches all classpath locations that contain the
|
||||
`com.mycompany` base package: `classpath*:com/mycompany/**/service-context.xml`.
|
||||
|
||||
|
||||
|
||||
[[resources-filesystemresource-caveats]]
|
||||
=== `FileSystemResource` Caveats
|
||||
|
||||
A `FileSystemResource` that is not attached to a `FileSystemApplicationContext` (that
|
||||
is, when a `FileSystemApplicationContext` is not the actual `ResourceLoader`) treats
|
||||
absolute and relative paths as you would expect. Relative paths are relative to the
|
||||
current working directory, while absolute paths are relative to the root of the
|
||||
filesystem.
|
||||
|
||||
For backwards compatibility (historical) reasons however, this changes when the
|
||||
`FileSystemApplicationContext` is the `ResourceLoader`. The
|
||||
`FileSystemApplicationContext` forces all attached `FileSystemResource` instances
|
||||
to treat all location paths as relative, whether they start with a leading slash or not.
|
||||
In practice, this means the following examples are equivalent:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ApplicationContext ctx =
|
||||
new FileSystemXmlApplicationContext("conf/context.xml");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val ctx = FileSystemXmlApplicationContext("conf/context.xml")
|
||||
----
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ApplicationContext ctx =
|
||||
new FileSystemXmlApplicationContext("/conf/context.xml");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val ctx = FileSystemXmlApplicationContext("/conf/context.xml")
|
||||
----
|
||||
|
||||
The following examples are also equivalent (even though it would make sense for them to be different, as one
|
||||
case is relative and the other absolute):
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
FileSystemXmlApplicationContext ctx = ...;
|
||||
ctx.getResource("some/resource/path/myTemplate.txt");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val ctx: FileSystemXmlApplicationContext = ...
|
||||
ctx.getResource("some/resource/path/myTemplate.txt")
|
||||
----
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
FileSystemXmlApplicationContext ctx = ...;
|
||||
ctx.getResource("/some/resource/path/myTemplate.txt");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val ctx: FileSystemXmlApplicationContext = ...
|
||||
ctx.getResource("/some/resource/path/myTemplate.txt")
|
||||
----
|
||||
|
||||
In practice, if you need true absolute filesystem paths, you should avoid using
|
||||
absolute paths with `FileSystemResource` or `FileSystemXmlApplicationContext` and
|
||||
force the use of a `UrlResource` by using the `file:` URL prefix. The following examples
|
||||
show how to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
// actual context type doesn't matter, the Resource will always be UrlResource
|
||||
ctx.getResource("file:///some/resource/path/myTemplate.txt");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
// actual context type doesn't matter, the Resource will always be UrlResource
|
||||
ctx.getResource("file:///some/resource/path/myTemplate.txt")
|
||||
----
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
|
||||
ApplicationContext ctx =
|
||||
new FileSystemXmlApplicationContext("file:///conf/context.xml");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
|
||||
val ctx = FileSystemXmlApplicationContext("file:///conf/context.xml")
|
||||
----
|
||||
@@ -1,41 +0,0 @@
|
||||
[[spring-jcl]]
|
||||
= Logging
|
||||
|
||||
Since Spring Framework 5.0, Spring comes with its own Commons Logging bridge implemented
|
||||
in the `spring-jcl` module. The implementation checks for the presence of the Log4j 2.x
|
||||
API and the SLF4J 1.7 API in the classpath and uses the first one of those found as the
|
||||
logging implementation, falling back to the Java platform's core logging facilities (also
|
||||
known as _JUL_ or `java.util.logging`) if neither Log4j 2.x nor SLF4J is available.
|
||||
|
||||
Put Log4j 2.x or Logback (or another SLF4J provider) in your classpath, without any extra
|
||||
bridges, and let the framework auto-adapt to your choice. For further information see the
|
||||
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-logging[Spring
|
||||
Boot Logging Reference Documentation].
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Spring's Commons Logging variant is only meant to be used for infrastructure logging
|
||||
purposes in the core framework and in extensions.
|
||||
|
||||
For logging needs within application code, prefer direct use of Log4j 2.x, SLF4J, or JUL.
|
||||
====
|
||||
|
||||
A `Log` implementation may be retrieved via `org.apache.commons.logging.LogFactory` as in
|
||||
the following example.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
public class MyBean {
|
||||
private final Log log = LogFactory.getLog(getClass());
|
||||
// ...
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class MyBean {
|
||||
private val log = LogFactory.getLog(javaClass)
|
||||
// ...
|
||||
}
|
||||
----
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
||||
<div id="header">
|
||||
<h1>Spring Framework Documentation</h1>
|
||||
<!--
|
||||
<div class="details">
|
||||
<span id="revnumber">{revnumber}</span>
|
||||
</div>
|
||||
-->
|
||||
<span><strong>{revnumber}</strong></span>
|
||||
</div>
|
||||
@@ -1,38 +0,0 @@
|
||||
:noheader:
|
||||
= Spring Framework Documentation
|
||||
include::attributes.adoc[]
|
||||
|
||||
[horizontal]
|
||||
<<overview.adoc#overview, Overview>> :: history, design philosophy, feedback,
|
||||
getting started.
|
||||
<<core.adoc#spring-core, Core>> :: IoC Container, Events, Resources, i18n,
|
||||
Validation, Data Binding, Type Conversion, SpEL, AOP, AOT.
|
||||
<<testing.adoc#testing, Testing>> :: Mock Objects, TestContext Framework,
|
||||
Spring MVC Test, WebTestClient.
|
||||
<<data-access.adoc#spring-data-tier, Data Access>> :: Transactions, DAO Support,
|
||||
JDBC, R2DBC, O/R Mapping, XML Marshalling.
|
||||
<<web.adoc#spring-web, Web Servlet>> :: Spring MVC, WebSocket, SockJS,
|
||||
STOMP Messaging.
|
||||
<<web-reactive.adoc#spring-webflux, Web Reactive>> :: Spring WebFlux, WebClient,
|
||||
WebSocket, RSocket.
|
||||
<<integration.adoc#spring-integration, Integration>> :: REST Clients, JMS, JCA, JMX,
|
||||
Email, Tasks, Scheduling, Caching.
|
||||
<<languages.adoc#languages, Languages>> :: Kotlin, Groovy, Dynamic Languages.
|
||||
<<appendix.adoc#appendix, Appendix>> :: Spring properties.
|
||||
https://github.com/spring-projects/spring-framework/wiki[*Wiki*] :: What's New,
|
||||
Upgrade Notes, Supported Versions, and other cross-version information.
|
||||
|
||||
NOTE: This documentation is available in {docs-spring-framework}/reference/html/index.html[HTML] and {docs-spring-framework}/reference/pdf/spring-framework.pdf[PDF] formats.
|
||||
|
||||
Rod Johnson, Juergen Hoeller, Keith Donald, Colin Sampaleanu, Rob Harrop, Thomas Risberg,
|
||||
Alef Arendsen, Darren Davison, Dmitriy Kopylenko, Mark Pollack, Thierry Templier, Erwin
|
||||
Vervaet, Portia Tung, Ben Hale, Adrian Colyer, John Lewis, Costin Leau, Mark Fisher, Sam
|
||||
Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clement, Dave
|
||||
Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane
|
||||
Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch
|
||||
|
||||
Copyright © 2002 - 2022 VMware, Inc. All Rights Reserved.
|
||||
|
||||
Copies of this document may be made for your own use and for distribution to others,
|
||||
provided that you do not charge any fee for such copies and further provided that each
|
||||
copy contains this Copyright Notice, whether distributed in print or electronically.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
||||
[[languages]]
|
||||
= Language Support
|
||||
include::attributes.adoc[]
|
||||
include::page-layout.adoc[]
|
||||
|
||||
include::languages/kotlin.adoc[leveloffset=+1]
|
||||
|
||||
include::languages/groovy.adoc[leveloffset=+1]
|
||||
|
||||
include::languages/dynamic-languages.adoc[leveloffset=+1]
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
:toc: left
|
||||
:toclevels: 4
|
||||
:tabsize: 4
|
||||
:docinfo1:
|
||||
@@ -1,981 +0,0 @@
|
||||
[[rsocket]]
|
||||
= RSocket
|
||||
include::attributes.adoc[]
|
||||
include::page-layout.adoc[]
|
||||
|
||||
This section describes Spring Framework's support for the RSocket protocol.
|
||||
|
||||
|
||||
[[rsocket-overview]]
|
||||
== Overview
|
||||
|
||||
RSocket is an application protocol for multiplexed, duplex communication over TCP,
|
||||
WebSocket, and other byte stream transports, using one of the following interaction
|
||||
models:
|
||||
|
||||
* `Request-Response` -- send one message and receive one back.
|
||||
* `Request-Stream` -- send one message and receive a stream of messages back.
|
||||
* `Channel` -- send streams of messages in both directions.
|
||||
* `Fire-and-Forget` -- send a one-way message.
|
||||
|
||||
Once the initial connection is made, the "client" vs "server" distinction is lost as
|
||||
both sides become symmetrical and each side can initiate one of the above interactions.
|
||||
This is why in the protocol calls the participating sides "requester" and "responder"
|
||||
while the above interactions are called "request streams" or simply "requests".
|
||||
|
||||
These are the key features and benefits of the RSocket protocol:
|
||||
|
||||
* https://www.reactive-streams.org/[Reactive Streams] semantics across network boundary --
|
||||
for streaming requests such as `Request-Stream` and `Channel`, back pressure signals
|
||||
travel between requester and responder, allowing a requester to slow down a responder at
|
||||
the source, hence reducing reliance on network layer congestion control, and the need
|
||||
for buffering at the network level or at any level.
|
||||
* Request throttling -- this feature is named "Leasing" after the `LEASE` frame that
|
||||
can be sent from each end to limit the total number of requests allowed by other end
|
||||
for a given time. Leases are renewed periodically.
|
||||
* Session resumption -- this is designed for loss of connectivity and requires some state
|
||||
to be maintained. The state management is transparent for applications, and works well
|
||||
in combination with back pressure which can stop a producer when possible and reduce
|
||||
the amount of state required.
|
||||
* Fragmentation and re-assembly of large messages.
|
||||
* Keepalive (heartbeats).
|
||||
|
||||
RSocket has {gh-rsocket}[implementations] in multiple languages. The
|
||||
{gh-rsocket-java}[Java library] is built on https://projectreactor.io/[Project Reactor],
|
||||
and https://github.com/reactor/reactor-netty[Reactor Netty] for the transport. That means
|
||||
signals from Reactive Streams Publishers in your application propagate transparently
|
||||
through RSocket across the network.
|
||||
|
||||
|
||||
|
||||
[[rsocket-protocol]]
|
||||
=== The Protocol
|
||||
|
||||
One of the benefits of RSocket is that it has well defined behavior on the wire and an
|
||||
easy to read https://rsocket.io/about/protocol[specification] along with some protocol
|
||||
{gh-rsocket}/rsocket/tree/master/Extensions[extensions]. Therefore it is
|
||||
a good idea to read the spec, independent of language implementations and higher level
|
||||
framework APIs. This section provides a succinct overview to establish some context.
|
||||
|
||||
**Connecting**
|
||||
|
||||
Initially a client connects to a server via some low level streaming transport such
|
||||
as TCP or WebSocket and sends a `SETUP` frame to the server to set parameters for the
|
||||
connection.
|
||||
|
||||
The server may reject the `SETUP` frame, but generally after it is sent (for the client)
|
||||
and received (for the server), both sides can begin to make requests, unless `SETUP`
|
||||
indicates use of leasing semantics to limit the number of requests, in which case
|
||||
both sides must wait for a `LEASE` frame from the other end to permit making requests.
|
||||
|
||||
**Making Requests**
|
||||
|
||||
Once a connection is established, both sides may initiate a request through one of the
|
||||
frames `REQUEST_RESPONSE`, `REQUEST_STREAM`, `REQUEST_CHANNEL`, or `REQUEST_FNF`. Each of
|
||||
those frames carries one message from the requester to the responder.
|
||||
|
||||
The responder may then return `PAYLOAD` frames with response messages, and in the case
|
||||
of `REQUEST_CHANNEL` the requester may also send `PAYLOAD` frames with more request
|
||||
messages.
|
||||
|
||||
When a request involves a stream of messages such as `Request-Stream` and `Channel`,
|
||||
the responder must respect demand signals from the requester. Demand is expressed as a
|
||||
number of messages. Initial demand is specified in `REQUEST_STREAM` and
|
||||
`REQUEST_CHANNEL` frames. Subsequent demand is signaled via `REQUEST_N` frames.
|
||||
|
||||
Each side may also send metadata notifications, via the `METADATA_PUSH` frame, that do not
|
||||
pertain to any individual request but rather to the connection as a whole.
|
||||
|
||||
**Message Format**
|
||||
|
||||
RSocket messages contain data and metadata. Metadata can be used to send a route, a
|
||||
security token, etc. Data and metadata can be formatted differently. Mime types for each
|
||||
are declared in the `SETUP` frame and apply to all requests on a given connection.
|
||||
|
||||
While all messages can have metadata, typically metadata such as a route are per-request
|
||||
and therefore only included in the first message on a request, i.e. with one of the frames
|
||||
`REQUEST_RESPONSE`, `REQUEST_STREAM`, `REQUEST_CHANNEL`, or `REQUEST_FNF`.
|
||||
|
||||
Protocol extensions define common metadata formats for use in applications:
|
||||
|
||||
* {gh-rsocket-extensions}/CompositeMetadata.md[Composite Metadata]-- multiple,
|
||||
independently formatted metadata entries.
|
||||
* {gh-rsocket-extensions}/Routing.md[Routing] -- the route for a request.
|
||||
|
||||
|
||||
|
||||
[[rsocket-java]]
|
||||
=== Java Implementation
|
||||
|
||||
The {gh-rsocket-java}[Java implementation] for RSocket is built on
|
||||
https://projectreactor.io/[Project Reactor]. The transports for TCP and WebSocket are
|
||||
built on https://github.com/reactor/reactor-netty[Reactor Netty]. As a Reactive Streams
|
||||
library, Reactor simplifies the job of implementing the protocol. For applications it is
|
||||
a natural fit to use `Flux` and `Mono` with declarative operators and transparent back
|
||||
pressure support.
|
||||
|
||||
The API in RSocket Java is intentionally minimal and basic. It focuses on protocol
|
||||
features and leaves the application programming model (e.g. RPC codegen vs other) as a
|
||||
higher level, independent concern.
|
||||
|
||||
The main contract
|
||||
{gh-rsocket-java}/blob/master/rsocket-core/src/main/java/io/rsocket/RSocket.java[io.rsocket.RSocket]
|
||||
models the four request interaction types with `Mono` representing a promise for a
|
||||
single message, `Flux` a stream of messages, and `io.rsocket.Payload` the actual
|
||||
message with access to data and metadata as byte buffers. The `RSocket` contract is used
|
||||
symmetrically. For requesting, the application is given an `RSocket` to perform
|
||||
requests with. For responding, the application implements `RSocket` to handle requests.
|
||||
|
||||
This is not meant to be a thorough introduction. For the most part, Spring applications
|
||||
will not have to use its API directly. However it may be important to see or experiment
|
||||
with RSocket independent of Spring. The RSocket Java repository contains a number of
|
||||
{gh-rsocket-java}/tree/master/rsocket-examples[sample apps] that
|
||||
demonstrate its API and protocol features.
|
||||
|
||||
|
||||
|
||||
[[rsocket-spring]]
|
||||
=== Spring Support
|
||||
|
||||
The `spring-messaging` module contains the following:
|
||||
|
||||
* <<rsocket-requester>> -- fluent API to make requests through an `io.rsocket.RSocket`
|
||||
with data and metadata encoding/decoding.
|
||||
* <<rsocket-annot-responders>> -- `@MessageMapping` annotated handler methods for
|
||||
responding.
|
||||
|
||||
The `spring-web` module contains `Encoder` and `Decoder` implementations such as Jackson
|
||||
CBOR/JSON, and Protobuf that RSocket applications will likely need. It also contains the
|
||||
`PathPatternParser` that can be plugged in for efficient route matching.
|
||||
|
||||
Spring Boot 2.2 supports standing up an RSocket server over TCP or WebSocket, including
|
||||
the option to expose RSocket over WebSocket in a WebFlux server. There is also client
|
||||
support and auto-configuration for an `RSocketRequester.Builder` and `RSocketStrategies`.
|
||||
See the
|
||||
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-rsocket[RSocket section]
|
||||
in the Spring Boot reference for more details.
|
||||
|
||||
Spring Security 5.2 provides RSocket support.
|
||||
|
||||
Spring Integration 5.2 provides inbound and outbound gateways to interact with RSocket
|
||||
clients and servers. See the Spring Integration Reference Manual for more details.
|
||||
|
||||
Spring Cloud Gateway supports RSocket connections.
|
||||
|
||||
|
||||
|
||||
[[rsocket-requester]]
|
||||
== RSocketRequester
|
||||
|
||||
`RSocketRequester` provides a fluent API to perform RSocket requests, accepting and
|
||||
returning objects for data and metadata instead of low level data buffers. It can be used
|
||||
symmetrically, to make requests from clients and to make requests from servers.
|
||||
|
||||
|
||||
[[rsocket-requester-client]]
|
||||
=== Client Requester
|
||||
|
||||
To obtain an `RSocketRequester` on the client side is to connect to a server which involves
|
||||
sending an RSocket `SETUP` frame with connection settings. `RSocketRequester` provides a
|
||||
builder that helps to prepare an `io.rsocket.core.RSocketConnector` including connection
|
||||
settings for the `SETUP` frame.
|
||||
|
||||
This is the most basic way to connect with default settings:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RSocketRequester requester = RSocketRequester.builder().tcp("localhost", 7000);
|
||||
|
||||
URI url = URI.create("https://example.org:8080/rsocket");
|
||||
RSocketRequester requester = RSocketRequester.builder().webSocket(url);
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val requester = RSocketRequester.builder().tcp("localhost", 7000)
|
||||
|
||||
URI url = URI.create("https://example.org:8080/rsocket");
|
||||
val requester = RSocketRequester.builder().webSocket(url)
|
||||
----
|
||||
|
||||
The above does not connect immediately. When requests are made, a shared connection is
|
||||
established transparently and used.
|
||||
|
||||
|
||||
[[rsocket-requester-client-setup]]
|
||||
==== Connection Setup
|
||||
|
||||
`RSocketRequester.Builder` provides the following to customize the initial `SETUP` frame:
|
||||
|
||||
* `dataMimeType(MimeType)` -- set the mime type for data on the connection.
|
||||
* `metadataMimeType(MimeType)` -- set the mime type for metadata on the connection.
|
||||
* `setupData(Object)` -- data to include in the `SETUP`.
|
||||
* `setupRoute(String, Object...)` -- route in the metadata to include in the `SETUP`.
|
||||
* `setupMetadata(Object, MimeType)` -- other metadata to include in the `SETUP`.
|
||||
|
||||
For data, the default mime type is derived from the first configured `Decoder`. For
|
||||
metadata, the default mime type is
|
||||
{gh-rsocket-extensions}/CompositeMetadata.md[composite metadata] which allows multiple
|
||||
metadata value and mime type pairs per request. Typically both don't need to be changed.
|
||||
|
||||
Data and metadata in the `SETUP` frame is optional. On the server side,
|
||||
<<rsocket-annot-connectmapping>> methods can be used to handle the start of a
|
||||
connection and the content of the `SETUP` frame. Metadata may be used for connection
|
||||
level security.
|
||||
|
||||
|
||||
[[rsocket-requester-client-strategies]]
|
||||
==== Strategies
|
||||
|
||||
`RSocketRequester.Builder` accepts `RSocketStrategies` to configure the requester.
|
||||
You'll need to use this to provide encoders and decoders for (de)-serialization of data and
|
||||
metadata values. By default only the basic codecs from `spring-core` for `String`,
|
||||
`byte[]`, and `ByteBuffer` are registered. Adding `spring-web` provides access to more that
|
||||
can be registered as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RSocketStrategies strategies = RSocketStrategies.builder()
|
||||
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
|
||||
.build();
|
||||
|
||||
RSocketRequester requester = RSocketRequester.builder()
|
||||
.rsocketStrategies(strategies)
|
||||
.tcp("localhost", 7000);
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val strategies = RSocketStrategies.builder()
|
||||
.encoders { it.add(Jackson2CborEncoder()) }
|
||||
.decoders { it.add(Jackson2CborDecoder()) }
|
||||
.build()
|
||||
|
||||
val requester = RSocketRequester.builder()
|
||||
.rsocketStrategies(strategies)
|
||||
.tcp("localhost", 7000)
|
||||
----
|
||||
|
||||
`RSocketStrategies` is designed for re-use. In some scenarios, e.g. client and server in
|
||||
the same application, it may be preferable to declare it in Spring configuration.
|
||||
|
||||
|
||||
[[rsocket-requester-client-responder]]
|
||||
==== Client Responders
|
||||
|
||||
`RSocketRequester.Builder` can be used to configure responders to requests from the
|
||||
server.
|
||||
|
||||
You can use annotated handlers for client-side responding based on the same
|
||||
infrastructure that's used on a server, but registered programmatically as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RSocketStrategies strategies = RSocketStrategies.builder()
|
||||
.routeMatcher(new PathPatternRouteMatcher()) // <1>
|
||||
.build();
|
||||
|
||||
SocketAcceptor responder =
|
||||
RSocketMessageHandler.responder(strategies, new ClientHandler()); // <2>
|
||||
|
||||
RSocketRequester requester = RSocketRequester.builder()
|
||||
.rsocketConnector(connector -> connector.acceptor(responder)) // <3>
|
||||
.tcp("localhost", 7000);
|
||||
----
|
||||
<1> Use `PathPatternRouteMatcher`, if `spring-web` is present, for efficient
|
||||
route matching.
|
||||
<2> Create a responder from a class with `@MessageMapping` and/or `@ConnectMapping` methods.
|
||||
<3> Register the responder.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val strategies = RSocketStrategies.builder()
|
||||
.routeMatcher(PathPatternRouteMatcher()) // <1>
|
||||
.build()
|
||||
|
||||
val responder =
|
||||
RSocketMessageHandler.responder(strategies, new ClientHandler()); // <2>
|
||||
|
||||
val requester = RSocketRequester.builder()
|
||||
.rsocketConnector { it.acceptor(responder) } // <3>
|
||||
.tcp("localhost", 7000)
|
||||
----
|
||||
<1> Use `PathPatternRouteMatcher`, if `spring-web` is present, for efficient
|
||||
route matching.
|
||||
<2> Create a responder from a class with `@MessageMapping` and/or `@ConnectMapping` methods.
|
||||
<3> Register the responder.
|
||||
|
||||
Note the above is only a shortcut designed for programmatic registration of client
|
||||
responders. For alternative scenarios, where client responders are in Spring configuration,
|
||||
you can still declare `RSocketMessageHandler` as a Spring bean and then apply as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ApplicationContext context = ... ;
|
||||
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
|
||||
|
||||
RSocketRequester requester = RSocketRequester.builder()
|
||||
.rsocketConnector(connector -> connector.acceptor(handler.responder()))
|
||||
.tcp("localhost", 7000);
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.beans.factory.getBean
|
||||
|
||||
val context: ApplicationContext = ...
|
||||
val handler = context.getBean<RSocketMessageHandler>()
|
||||
|
||||
val requester = RSocketRequester.builder()
|
||||
.rsocketConnector { it.acceptor(handler.responder()) }
|
||||
.tcp("localhost", 7000)
|
||||
----
|
||||
|
||||
For the above you may also need to use `setHandlerPredicate` in `RSocketMessageHandler` to
|
||||
switch to a different strategy for detecting client responders, e.g. based on a custom
|
||||
annotation such as `@RSocketClientResponder` vs the default `@Controller`. This
|
||||
is necessary in scenarios with client and server, or multiple clients in the same
|
||||
application.
|
||||
|
||||
See also <<rsocket-annot-responders>>, for more on the programming model.
|
||||
|
||||
|
||||
[[rsocket-requester-client-advanced]]
|
||||
==== Advanced
|
||||
|
||||
`RSocketRequesterBuilder` provides a callback to expose the underlying
|
||||
`io.rsocket.core.RSocketConnector` for further configuration options for keepalive
|
||||
intervals, session resumption, interceptors, and more. You can configure options
|
||||
at that level as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RSocketRequester requester = RSocketRequester.builder()
|
||||
.rsocketConnector(connector -> {
|
||||
// ...
|
||||
})
|
||||
.tcp("localhost", 7000);
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val requester = RSocketRequester.builder()
|
||||
.rsocketConnector {
|
||||
//...
|
||||
}
|
||||
.tcp("localhost", 7000)
|
||||
----
|
||||
|
||||
|
||||
[[rsocket-requester-server]]
|
||||
=== Server Requester
|
||||
|
||||
To make requests from a server to connected clients is a matter of obtaining the
|
||||
requester for the connected client from the server.
|
||||
|
||||
In <<rsocket-annot-responders>>, `@ConnectMapping` and `@MessageMapping` methods support an
|
||||
`RSocketRequester` argument. Use it to access the requester for the connection. Keep in
|
||||
mind that `@ConnectMapping` methods are essentially handlers of the `SETUP` frame which
|
||||
must be handled before requests can begin. Therefore, requests at the very start must be
|
||||
decoupled from handling. For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@ConnectMapping
|
||||
Mono<Void> handle(RSocketRequester requester) {
|
||||
requester.route("status").data("5")
|
||||
.retrieveFlux(StatusReport.class)
|
||||
.subscribe(bar -> { // <1>
|
||||
// ...
|
||||
});
|
||||
return ... // <2>
|
||||
}
|
||||
----
|
||||
<1> Start the request asynchronously, independent from handling.
|
||||
<2> Perform handling and return completion `Mono<Void>`.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@ConnectMapping
|
||||
suspend fun handle(requester: RSocketRequester) {
|
||||
GlobalScope.launch {
|
||||
requester.route("status").data("5").retrieveFlow<StatusReport>().collect { // <1>
|
||||
// ...
|
||||
}
|
||||
}
|
||||
/// ... <2>
|
||||
}
|
||||
----
|
||||
<1> Start the request asynchronously, independent from handling.
|
||||
<2> Perform handling in the suspending function.
|
||||
|
||||
|
||||
|
||||
[[rsocket-requester-requests]]
|
||||
=== Requests
|
||||
|
||||
Once you have a <<rsocket-requester-client,client>> or
|
||||
<<rsocket-requester-server,server>> requester, you can make requests as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ViewBox viewBox = ... ;
|
||||
|
||||
Flux<AirportLocation> locations = requester.route("locate.radars.within") // <1>
|
||||
.data(viewBox) // <2>
|
||||
.retrieveFlux(AirportLocation.class); // <3>
|
||||
|
||||
----
|
||||
<1> Specify a route to include in the metadata of the request message.
|
||||
<2> Provide data for the request message.
|
||||
<3> Declare the expected response.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val viewBox: ViewBox = ...
|
||||
|
||||
val locations = requester.route("locate.radars.within") // <1>
|
||||
.data(viewBox) // <2>
|
||||
.retrieveFlow<AirportLocation>() // <3>
|
||||
----
|
||||
<1> Specify a route to include in the metadata of the request message.
|
||||
<2> Provide data for the request message.
|
||||
<3> Declare the expected response.
|
||||
|
||||
The interaction type is determined implicitly from the cardinality of the input and
|
||||
output. The above example is a `Request-Stream` because one value is sent and a stream
|
||||
of values is received. For the most part you don't need to think about this as long as the
|
||||
choice of input and output matches an RSocket interaction type and the types of input and
|
||||
output expected by the responder. The only example of an invalid combination is many-to-one.
|
||||
|
||||
The `data(Object)` method also accepts any Reactive Streams `Publisher`, including
|
||||
`Flux` and `Mono`, as well as any other producer of value(s) that is registered in the
|
||||
`ReactiveAdapterRegistry`. For a multi-value `Publisher` such as `Flux` which produces the
|
||||
same types of values, consider using one of the overloaded `data` methods to avoid having
|
||||
type checks and `Encoder` lookup on every element:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
data(Object producer, Class<?> elementClass);
|
||||
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);
|
||||
----
|
||||
|
||||
The `data(Object)` step is optional. Skip it for requests that don't send data:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<AirportLocation> location = requester.route("find.radar.EWR"))
|
||||
.retrieveMono(AirportLocation.class);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.messaging.rsocket.retrieveAndAwait
|
||||
|
||||
val location = requester.route("find.radar.EWR")
|
||||
.retrieveAndAwait<AirportLocation>()
|
||||
----
|
||||
|
||||
Extra metadata values can be added if using
|
||||
{gh-rsocket-extensions}/CompositeMetadata.md[composite metadata] (the default) and if the
|
||||
values are supported by a registered `Encoder`. For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
String securityToken = ... ;
|
||||
ViewBox viewBox = ... ;
|
||||
MimeType mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0");
|
||||
|
||||
Flux<AirportLocation> locations = requester.route("locate.radars.within")
|
||||
.metadata(securityToken, mimeType)
|
||||
.data(viewBox)
|
||||
.retrieveFlux(AirportLocation.class);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.messaging.rsocket.retrieveFlow
|
||||
|
||||
val requester: RSocketRequester = ...
|
||||
|
||||
val securityToken: String = ...
|
||||
val viewBox: ViewBox = ...
|
||||
val mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0")
|
||||
|
||||
val locations = requester.route("locate.radars.within")
|
||||
.metadata(securityToken, mimeType)
|
||||
.data(viewBox)
|
||||
.retrieveFlow<AirportLocation>()
|
||||
----
|
||||
|
||||
For `Fire-and-Forget` use the `send()` method that returns `Mono<Void>`. Note that the `Mono`
|
||||
indicates only that the message was successfully sent, and not that it was handled.
|
||||
|
||||
For `Metadata-Push` use the `sendMetadata()` method with a `Mono<Void>` return value.
|
||||
|
||||
|
||||
|
||||
[[rsocket-annot-responders]]
|
||||
== Annotated Responders
|
||||
|
||||
RSocket responders can be implemented as `@MessageMapping` and `@ConnectMapping` methods.
|
||||
`@MessageMapping` methods handle individual requests while `@ConnectMapping` methods handle
|
||||
connection-level events (setup and metadata push). Annotated responders are supported
|
||||
symmetrically, for responding from the server side and for responding from the client side.
|
||||
|
||||
|
||||
|
||||
[[rsocket-annot-responders-server]]
|
||||
=== Server Responders
|
||||
|
||||
To use annotated responders on the server side, add `RSocketMessageHandler` to your Spring
|
||||
configuration to detect `@Controller` beans with `@MessageMapping` and `@ConnectMapping`
|
||||
methods:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
static class ServerConfig {
|
||||
|
||||
@Bean
|
||||
public RSocketMessageHandler rsocketMessageHandler() {
|
||||
RSocketMessageHandler handler = new RSocketMessageHandler();
|
||||
handler.routeMatcher(new PathPatternRouteMatcher());
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
class ServerConfig {
|
||||
|
||||
@Bean
|
||||
fun rsocketMessageHandler() = RSocketMessageHandler().apply {
|
||||
routeMatcher = PathPatternRouteMatcher()
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Then start an RSocket server through the Java RSocket API and plug the
|
||||
`RSocketMessageHandler` for the responder as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
ApplicationContext context = ... ;
|
||||
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
|
||||
|
||||
CloseableChannel server =
|
||||
RSocketServer.create(handler.responder())
|
||||
.bind(TcpServerTransport.create("localhost", 7000))
|
||||
.block();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.beans.factory.getBean
|
||||
|
||||
val context: ApplicationContext = ...
|
||||
val handler = context.getBean<RSocketMessageHandler>()
|
||||
|
||||
val server = RSocketServer.create(handler.responder())
|
||||
.bind(TcpServerTransport.create("localhost", 7000))
|
||||
.awaitSingle()
|
||||
----
|
||||
|
||||
`RSocketMessageHandler` supports
|
||||
{gh-rsocket-extensions}/CompositeMetadata.md[composite] and
|
||||
{gh-rsocket-extensions}/Routing.md[routing] metadata by default. You can set its
|
||||
<<rsocket-metadata-extractor>> if you need to switch to a
|
||||
different mime type or register additional metadata mime types.
|
||||
|
||||
You'll need to set the `Encoder` and `Decoder` instances required for metadata and data
|
||||
formats to support. You'll likely need the `spring-web` module for codec implementations.
|
||||
|
||||
By default `SimpleRouteMatcher` is used for matching routes via `AntPathMatcher`.
|
||||
We recommend plugging in the `PathPatternRouteMatcher` from `spring-web` for
|
||||
efficient route matching. RSocket routes can be hierarchical but are not URL paths.
|
||||
Both route matchers are configured to use "." as separator by default and there is no URL
|
||||
decoding as with HTTP URLs.
|
||||
|
||||
`RSocketMessageHandler` can be configured via `RSocketStrategies` which may be useful if
|
||||
you need to share configuration between a client and a server in the same process:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
static class ServerConfig {
|
||||
|
||||
@Bean
|
||||
public RSocketMessageHandler rsocketMessageHandler() {
|
||||
RSocketMessageHandler handler = new RSocketMessageHandler();
|
||||
handler.setRSocketStrategies(rsocketStrategies());
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RSocketStrategies rsocketStrategies() {
|
||||
return RSocketStrategies.builder()
|
||||
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
|
||||
.routeMatcher(new PathPatternRouteMatcher())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
class ServerConfig {
|
||||
|
||||
@Bean
|
||||
fun rsocketMessageHandler() = RSocketMessageHandler().apply {
|
||||
rSocketStrategies = rsocketStrategies()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun rsocketStrategies() = RSocketStrategies.builder()
|
||||
.encoders { it.add(Jackson2CborEncoder()) }
|
||||
.decoders { it.add(Jackson2CborDecoder()) }
|
||||
.routeMatcher(PathPatternRouteMatcher())
|
||||
.build()
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[rsocket-annot-responders-client]]
|
||||
=== Client Responders
|
||||
|
||||
Annotated responders on the client side need to be configured in the
|
||||
`RSocketRequester.Builder`. For details, see
|
||||
<<rsocket-requester-client-responder>>.
|
||||
|
||||
|
||||
|
||||
[[rsocket-annot-messagemapping]]
|
||||
=== @MessageMapping
|
||||
|
||||
Once <<rsocket-annot-responders-server,server>> or
|
||||
<<rsocket-annot-responders-client,client>> responder configuration is in place,
|
||||
`@MessageMapping` methods can be used as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Controller
|
||||
public class RadarsController {
|
||||
|
||||
@MessageMapping("locate.radars.within")
|
||||
public Flux<AirportLocation> radars(MapRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Controller
|
||||
class RadarsController {
|
||||
|
||||
@MessageMapping("locate.radars.within")
|
||||
fun radars(request: MapRequest): Flow<AirportLocation> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The above `@MessageMapping` method responds to a Request-Stream interaction having the
|
||||
route "locate.radars.within". It supports a flexible method signature with the option to
|
||||
use the following method arguments:
|
||||
|
||||
[cols="1,3",options="header"]
|
||||
|===
|
||||
| Method Argument
|
||||
| Description
|
||||
|
||||
| `@Payload`
|
||||
| The payload of the request. This can be a concrete value of asynchronous types like
|
||||
`Mono` or `Flux`.
|
||||
|
||||
*Note:* Use of the annotation is optional. A method argument that is not a simple type
|
||||
and is not any of the other supported arguments, is assumed to be the expected payload.
|
||||
|
||||
| `RSocketRequester`
|
||||
| Requester for making requests to the remote end.
|
||||
|
||||
| `@DestinationVariable`
|
||||
| Value extracted from the route based on variables in the mapping pattern, e.g.
|
||||
pass:q[`@MessageMapping("find.radar.{id}")`].
|
||||
|
||||
| `@Header`
|
||||
| Metadata value registered for extraction as described in <<rsocket-metadata-extractor>>.
|
||||
|
||||
| `@Headers Map<String, Object>`
|
||||
| All metadata values registered for extraction as described in <<rsocket-metadata-extractor>>.
|
||||
|
||||
|===
|
||||
|
||||
The return value is expected to be one or more Objects to be serialized as response
|
||||
payloads. That can be asynchronous types like `Mono` or `Flux`, a concrete value, or
|
||||
either `void` or a no-value asynchronous type such as `Mono<Void>`.
|
||||
|
||||
The RSocket interaction type that an `@MessageMapping` method supports is determined from
|
||||
the cardinality of the input (i.e. `@Payload` argument) and of the output, where
|
||||
cardinality means the following:
|
||||
|
||||
[%autowidth]
|
||||
[cols=2*,options="header"]
|
||||
|===
|
||||
| Cardinality
|
||||
| Description
|
||||
|
||||
| 1
|
||||
| Either an explicit value, or a single-value asynchronous type such as `Mono<T>`.
|
||||
|
||||
| Many
|
||||
| A multi-value asynchronous type such as `Flux<T>`.
|
||||
|
||||
| 0
|
||||
| For input this means the method does not have an `@Payload` argument.
|
||||
|
||||
For output this is `void` or a no-value asynchronous type such as `Mono<Void>`.
|
||||
|===
|
||||
|
||||
The table below shows all input and output cardinality combinations and the corresponding
|
||||
interaction type(s):
|
||||
|
||||
[%autowidth]
|
||||
[cols=3*,options="header"]
|
||||
|===
|
||||
| Input Cardinality
|
||||
| Output Cardinality
|
||||
| Interaction Types
|
||||
|
||||
| 0, 1
|
||||
| 0
|
||||
| Fire-and-Forget, Request-Response
|
||||
|
||||
| 0, 1
|
||||
| 1
|
||||
| Request-Response
|
||||
|
||||
| 0, 1
|
||||
| Many
|
||||
| Request-Stream
|
||||
|
||||
| Many
|
||||
| 0, 1, Many
|
||||
| Request-Channel
|
||||
|
||||
|===
|
||||
|
||||
|
||||
|
||||
[[rsocket-annot-connectmapping]]
|
||||
=== @ConnectMapping
|
||||
|
||||
`@ConnectMapping` handles the `SETUP` frame at the start of an RSocket connection, and
|
||||
any subsequent metadata push notifications through the `METADATA_PUSH` frame, i.e.
|
||||
`metadataPush(Payload)` in `io.rsocket.RSocket`.
|
||||
|
||||
`@ConnectMapping` methods support the same arguments as
|
||||
<<rsocket-annot-messagemapping>> but based on metadata and data from the `SETUP` and
|
||||
`METADATA_PUSH` frames. `@ConnectMapping` can have a pattern to narrow handling to
|
||||
specific connections that have a route in the metadata, or if no patterns are declared
|
||||
then all connections match.
|
||||
|
||||
`@ConnectMapping` methods cannot return data and must be declared with `void` or
|
||||
`Mono<Void>` as the return value. If handling returns an error for a new
|
||||
connection then the connection is rejected. Handling must not be held up to make
|
||||
requests to the `RSocketRequester` for the connection. See
|
||||
<<rsocket-requester-server>> for details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[rsocket-metadata-extractor]]
|
||||
== MetadataExtractor
|
||||
|
||||
Responders must interpret metadata.
|
||||
{gh-rsocket-extensions}/CompositeMetadata.md[Composite metadata] allows independently
|
||||
formatted metadata values (e.g. for routing, security, tracing) each with its own mime
|
||||
type. Applications need a way to configure metadata mime types to support, and a way
|
||||
to access extracted values.
|
||||
|
||||
`MetadataExtractor` is a contract to take serialized metadata and return decoded
|
||||
name-value pairs that can then be accessed like headers by name, for example via `@Header`
|
||||
in annotated handler methods.
|
||||
|
||||
`DefaultMetadataExtractor` can be given `Decoder` instances to decode metadata. Out of
|
||||
the box it has built-in support for
|
||||
{gh-rsocket-extensions}/Routing.md["message/x.rsocket.routing.v0"] which it decodes to
|
||||
`String` and saves under the "route" key. For any other mime type you'll need to provide
|
||||
a `Decoder` and register the mime type as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
|
||||
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.messaging.rsocket.metadataToExtract
|
||||
|
||||
val extractor = DefaultMetadataExtractor(metadataDecoders)
|
||||
extractor.metadataToExtract<Foo>(fooMimeType, "foo")
|
||||
----
|
||||
|
||||
Composite metadata works well to combine independent metadata values. However the
|
||||
requester might not support composite metadata, or may choose not to use it. For this,
|
||||
`DefaultMetadataExtractor` may needs custom logic to map the decoded value to the output
|
||||
map. Here is an example where JSON is used for metadata:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
|
||||
extractor.metadataToExtract(
|
||||
MimeType.valueOf("application/vnd.myapp.metadata+json"),
|
||||
new ParameterizedTypeReference<Map<String,String>>() {},
|
||||
(jsonMap, outputMap) -> {
|
||||
outputMap.putAll(jsonMap);
|
||||
});
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.messaging.rsocket.metadataToExtract
|
||||
|
||||
val extractor = DefaultMetadataExtractor(metadataDecoders)
|
||||
extractor.metadataToExtract<Map<String, String>>(MimeType.valueOf("application/vnd.myapp.metadata+json")) { jsonMap, outputMap ->
|
||||
outputMap.putAll(jsonMap)
|
||||
}
|
||||
----
|
||||
|
||||
When configuring `MetadataExtractor` through `RSocketStrategies`, you can let
|
||||
`RSocketStrategies.Builder` create the extractor with the configured decoders, and
|
||||
simply use a callback to customize registrations as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RSocketStrategies strategies = RSocketStrategies.builder()
|
||||
.metadataExtractorRegistry(registry -> {
|
||||
registry.metadataToExtract(fooMimeType, Foo.class, "foo");
|
||||
// ...
|
||||
})
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.messaging.rsocket.metadataToExtract
|
||||
|
||||
val strategies = RSocketStrategies.builder()
|
||||
.metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
|
||||
registry.metadataToExtract<Foo>(fooMimeType, "foo")
|
||||
// ...
|
||||
}
|
||||
.build()
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[rsocket-interface]]
|
||||
== RSocket Interface
|
||||
|
||||
The Spring Framework lets you define an RSocket service as a Java interface with annotated
|
||||
methods for RSocket exchanges. You can then generate a proxy that implements this interface
|
||||
and performs the exchanges. This helps to simplify RSocket remote access by wrapping the
|
||||
use of the underlying <<rsocket-requester>>.
|
||||
|
||||
One, declare an interface with `@RSocketExchange` methods:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
interface RadarService {
|
||||
|
||||
@RSocketExchange("radars")
|
||||
Flux<AirportLocation> getRadars(@Payload MapRequest request);
|
||||
|
||||
// more RSocket exchange methods...
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Two, create a proxy that will perform the declared RSocket exchanges:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RSocketRequester requester = ... ;
|
||||
RSocketServiceProxyFactory factory = RSocketServiceProxyFactory.builder(requester).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RadarService.class);
|
||||
----
|
||||
|
||||
|
||||
[[rsocket-interface-method-parameters]]
|
||||
=== Method Parameters
|
||||
|
||||
Annotated, RSocket exchange methods support flexible method signatures with the following
|
||||
method parameters:
|
||||
|
||||
[cols="1,2", options="header"]
|
||||
|===
|
||||
| Method argument | Description
|
||||
|
||||
| `@DestinationVariable`
|
||||
| Add a route variable to pass to `RSocketRequester` along with the route from the
|
||||
`@RSocketExchange` annotation in order to expand template placeholders in the route.
|
||||
This variable can be a String or any Object, which is then formatted via `toString()`.
|
||||
|
||||
| `@Payload`
|
||||
| Set the input payload(s) for the request. This can be a concrete value, or any producer
|
||||
of values that can be adapted to a Reactive Streams `Publisher` via
|
||||
`ReactiveAdapterRegistry`
|
||||
|
||||
| `Object`, if followed by `MimeType`
|
||||
| The value for a metadata entry in the input payload. This can be any `Object` as long
|
||||
as the next argument is the metadata entry `MimeType`. The value can be a concrete
|
||||
value or any producer of a single value that can be adapted to a Reactive Streams
|
||||
`Publisher` via `ReactiveAdapterRegistry`.
|
||||
|
||||
| `MimeType`
|
||||
| The `MimeType` for a metadata entry. The preceding method argument is expected to be
|
||||
the metadata value.
|
||||
|
||||
|===
|
||||
|
||||
|
||||
[[rsocket-interface-return-values]]
|
||||
=== Return Values
|
||||
|
||||
Annotated, RSocket exchange methods support return values that are concrete value(s), or
|
||||
any producer of value(s) that can be adapted to a Reactive Streams `Publisher` via
|
||||
`ReactiveAdapterRegistry`.
|
||||
@@ -1,26 +0,0 @@
|
||||
:noheader:
|
||||
= Spring Framework Documentation
|
||||
include::attributes.adoc[]
|
||||
|
||||
include::overview.adoc[leveloffset=+1]
|
||||
include::core.adoc[leveloffset=+1]
|
||||
include::testing.adoc[leveloffset=+1]
|
||||
include::data-access.adoc[leveloffset=+1]
|
||||
include::web.adoc[leveloffset=+1]
|
||||
include::web-reactive.adoc[leveloffset=+1]
|
||||
include::integration.adoc[leveloffset=+1]
|
||||
include::languages.adoc[leveloffset=+1]
|
||||
include::appendix.adoc[leveloffset=+1]
|
||||
|
||||
Rod Johnson, Juergen Hoeller, Keith Donald, Colin Sampaleanu, Rob Harrop, Thomas Risberg,
|
||||
Alef Arendsen, Darren Davison, Dmitriy Kopylenko, Mark Pollack, Thierry Templier, Erwin
|
||||
Vervaet, Portia Tung, Ben Hale, Adrian Colyer, John Lewis, Costin Leau, Mark Fisher, Sam
|
||||
Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clement, Dave
|
||||
Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane
|
||||
Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch
|
||||
|
||||
Copyright © 2002 - 2022 VMware, Inc. All Rights Reserved.
|
||||
|
||||
Copies of this document may be made for your own use and for distribution to others,
|
||||
provided that you do not charge any fee for such copies and further provided that each
|
||||
copy contains this Copyright Notice, whether distributed in print or electronically.
|
||||
@@ -1,30 +0,0 @@
|
||||
[[testing]]
|
||||
= Testing
|
||||
include::attributes.adoc[]
|
||||
include::page-layout.adoc[]
|
||||
|
||||
This chapter covers Spring's support for integration testing and best practices for unit
|
||||
testing. The Spring team advocates test-driven development (TDD). The Spring team has
|
||||
found that the correct use of inversion of control (IoC) certainly does make both unit
|
||||
and integration testing easier (in that the presence of setter methods and appropriate
|
||||
constructors on classes makes them easier to wire together in a test without having to
|
||||
set up service locator registries and similar structures).
|
||||
|
||||
|
||||
include::testing/testing-introduction.adoc[leveloffset=+1]
|
||||
|
||||
include::testing/unit-testing.adoc[leveloffset=+1]
|
||||
|
||||
include::testing/integration-testing.adoc[leveloffset=+1]
|
||||
|
||||
include::testing/testing-support-jdbc.adoc[leveloffset=+1]
|
||||
|
||||
include::testing/testcontext-framework.adoc[leveloffset=+1]
|
||||
|
||||
include::testing/testing-webtestclient.adoc[leveloffset=+1]
|
||||
|
||||
include::testing/spring-mvc-test-framework.adoc[leveloffset=+1]
|
||||
|
||||
include::testing/spring-mvc-test-client.adoc[leveloffset=+1]
|
||||
|
||||
include::testing/testing-appendix.adoc[leveloffset=+1]
|
||||
@@ -1,155 +0,0 @@
|
||||
[[integration-testing]]
|
||||
= Integration Testing
|
||||
|
||||
It is important to be able to perform some integration testing without requiring
|
||||
deployment to your application server or connecting to other enterprise infrastructure.
|
||||
Doing so lets you test things such as:
|
||||
|
||||
* The correct wiring of your Spring IoC container contexts.
|
||||
* Data access using JDBC or an ORM tool. This can include such things as the correctness
|
||||
of SQL statements, Hibernate queries, JPA entity mappings, and so forth.
|
||||
|
||||
The Spring Framework provides first-class support for integration testing in the
|
||||
`spring-test` module. The name of the actual JAR file might include the release version
|
||||
and might also be in the long `org.springframework.test` form, depending on where you get
|
||||
it from (see the <<core.adoc#beans-dependencies, section on Dependency Management>>
|
||||
for an explanation). This library includes the `org.springframework.test` package, which
|
||||
contains valuable classes for integration testing with a Spring container. This testing
|
||||
does not rely on an application server or other deployment environment. Such tests are
|
||||
slower to run than unit tests but much faster than the equivalent Selenium tests or
|
||||
remote tests that rely on deployment to an application server.
|
||||
|
||||
Unit and integration testing support is provided in the form of the annotation-driven
|
||||
<<testcontext-framework, Spring TestContext Framework>>. The TestContext framework is
|
||||
agnostic of the actual testing framework in use, which allows instrumentation of tests
|
||||
in various environments, including JUnit, TestNG, and others.
|
||||
|
||||
The following section provides an overview of the high-level goals of Spring's
|
||||
integration support, and the rest of this chapter then focuses on dedicated topics:
|
||||
|
||||
* <<integration-testing-support-jdbc>>
|
||||
* <<testcontext-framework>>
|
||||
* <<webtestclient>>
|
||||
* <<spring-mvc-test-framework>>
|
||||
* <<spring-mvc-test-client>>
|
||||
* <<integration-testing-annotations>>
|
||||
|
||||
|
||||
|
||||
[[integration-testing-goals]]
|
||||
== Goals of Integration Testing
|
||||
|
||||
Spring's integration testing support has the following primary goals:
|
||||
|
||||
* To manage <<testing-ctx-management, Spring IoC container caching>> between tests.
|
||||
* To provide <<testing-fixture-di, Dependency Injection of test fixture instances>>.
|
||||
* To provide <<testing-tx, transaction management>> appropriate to integration testing.
|
||||
* To supply <<testing-support-classes, Spring-specific base classes>> that assist
|
||||
developers in writing integration tests.
|
||||
|
||||
The next few sections describe each goal and provide links to implementation and
|
||||
configuration details.
|
||||
|
||||
|
||||
[[testing-ctx-management]]
|
||||
=== Context Management and Caching
|
||||
|
||||
The Spring TestContext Framework provides consistent loading of Spring
|
||||
`ApplicationContext` instances and `WebApplicationContext` instances as well as caching
|
||||
of those contexts. Support for the caching of loaded contexts is important, because
|
||||
startup time can become an issue -- not because of the overhead of Spring itself, but
|
||||
because the objects instantiated by the Spring container take time to instantiate. For
|
||||
example, a project with 50 to 100 Hibernate mapping files might take 10 to 20 seconds to
|
||||
load the mapping files, and incurring that cost before running every test in every test
|
||||
fixture leads to slower overall test runs that reduce developer productivity.
|
||||
|
||||
Test classes typically declare either an array of resource locations for XML or Groovy
|
||||
configuration metadata -- often in the classpath -- or an array of component classes that
|
||||
is used to configure the application. These locations or classes are the same as or
|
||||
similar to those specified in `web.xml` or other configuration files for production
|
||||
deployments.
|
||||
|
||||
By default, once loaded, the configured `ApplicationContext` is reused for each test.
|
||||
Thus, the setup cost is incurred only once per test suite, and subsequent test execution
|
||||
is much faster. In this context, the term "`test suite`" means all tests run in the same
|
||||
JVM -- for example, all tests run from an Ant, Maven, or Gradle build for a given project
|
||||
or module. In the unlikely case that a test corrupts the application context and requires
|
||||
reloading (for example, by modifying a bean definition or the state of an application
|
||||
object) the TestContext framework can be configured to reload the configuration and
|
||||
rebuild the application context before executing the next test.
|
||||
|
||||
See <<testcontext-ctx-management>> and <<testcontext-ctx-management-caching>> with the
|
||||
TestContext framework.
|
||||
|
||||
|
||||
[[testing-fixture-di]]
|
||||
=== Dependency Injection of Test Fixtures
|
||||
|
||||
When the TestContext framework loads your application context, it can optionally
|
||||
configure instances of your test classes by using Dependency Injection. This provides a
|
||||
convenient mechanism for setting up test fixtures by using preconfigured beans from your
|
||||
application context. A strong benefit here is that you can reuse application contexts
|
||||
across various testing scenarios (for example, for configuring Spring-managed object
|
||||
graphs, transactional proxies, `DataSource` instances, and others), thus avoiding the
|
||||
need to duplicate complex test fixture setup for individual test cases.
|
||||
|
||||
As an example, consider a scenario where we have a class (`HibernateTitleRepository`)
|
||||
that implements data access logic for a `Title` domain entity. We want to write
|
||||
integration tests that test the following areas:
|
||||
|
||||
* The Spring configuration: Basically, is everything related to the configuration of the
|
||||
`HibernateTitleRepository` bean correct and present?
|
||||
* The Hibernate mapping file configuration: Is everything mapped correctly and are the
|
||||
correct lazy-loading settings in place?
|
||||
* The logic of the `HibernateTitleRepository`: Does the configured instance of this class
|
||||
perform as anticipated?
|
||||
|
||||
See dependency injection of test fixtures with the
|
||||
<<testcontext-fixture-di, TestContext framework>>.
|
||||
|
||||
|
||||
[[testing-tx]]
|
||||
=== Transaction Management
|
||||
|
||||
One common issue in tests that access a real database is their effect on the state of the
|
||||
persistence store. Even when you use a development database, changes to the state may
|
||||
affect future tests. Also, many operations -- such as inserting or modifying persistent
|
||||
data -- cannot be performed (or verified) outside of a transaction.
|
||||
|
||||
The TestContext framework addresses this issue. By default, the framework creates and
|
||||
rolls back a transaction for each test. You can write code that can assume the existence
|
||||
of a transaction. If you call transactionally proxied objects in your tests, they behave
|
||||
correctly, according to their configured transactional semantics. In addition, if a test
|
||||
method deletes the contents of selected tables while running within the transaction
|
||||
managed for the test, the transaction rolls back by default, and the database returns to
|
||||
its state prior to execution of the test. Transactional support is provided to a test by
|
||||
using a `PlatformTransactionManager` bean defined in the test's application context.
|
||||
|
||||
If you want a transaction to commit (unusual, but occasionally useful when you want a
|
||||
particular test to populate or modify the database), you can tell the TestContext
|
||||
framework to cause the transaction to commit instead of roll back by using the
|
||||
<<integration-testing-annotations, `@Commit`>> annotation.
|
||||
|
||||
See transaction management with the <<testcontext-tx, TestContext framework>>.
|
||||
|
||||
|
||||
[[testing-support-classes]]
|
||||
=== Support Classes for Integration Testing
|
||||
|
||||
The Spring TestContext Framework provides several `abstract` support classes that
|
||||
simplify the writing of integration tests. These base test classes provide well-defined
|
||||
hooks into the testing framework as well as convenient instance variables and methods,
|
||||
which let you access:
|
||||
|
||||
* The `ApplicationContext`, for performing explicit bean lookups or testing the state of
|
||||
the context as a whole.
|
||||
* A `JdbcTemplate`, for executing SQL statements to query the database. You can use such
|
||||
queries to confirm database state both before and after execution of database-related
|
||||
application code, and Spring ensures that such queries run in the scope of the same
|
||||
transaction as the application code. When used in conjunction with an ORM tool, be sure
|
||||
to avoid <<testcontext-tx-false-positives, false positives>>.
|
||||
|
||||
In addition, you may want to create your own custom, application-wide superclass with
|
||||
instance variables and methods specific to your project.
|
||||
|
||||
See support classes for the <<testcontext-support-classes, TestContext framework>>.
|
||||
@@ -1,136 +0,0 @@
|
||||
[[spring-mvc-test-client]]
|
||||
= Testing Client Applications
|
||||
|
||||
You can use client-side tests to test code that internally uses the `RestTemplate`. The
|
||||
idea is to declare expected requests and to provide "`stub`" responses so that you can
|
||||
focus on testing the code in isolation (that is, without running a server). The following
|
||||
example shows how to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
|
||||
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess());
|
||||
|
||||
// Test code that uses the above RestTemplate ...
|
||||
|
||||
mockServer.verify();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val restTemplate = RestTemplate()
|
||||
|
||||
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
|
||||
mockServer.expect(requestTo("/greeting")).andRespond(withSuccess())
|
||||
|
||||
// Test code that uses the above RestTemplate ...
|
||||
|
||||
mockServer.verify()
|
||||
----
|
||||
|
||||
In the preceding example, `MockRestServiceServer` (the central class for client-side REST
|
||||
tests) configures the `RestTemplate` with a custom `ClientHttpRequestFactory` that
|
||||
asserts actual requests against expectations and returns "`stub`" responses. In this
|
||||
case, we expect a request to `/greeting` and want to return a 200 response with
|
||||
`text/plain` content. We can define additional expected requests and stub responses as
|
||||
needed. When we define expected requests and stub responses, the `RestTemplate` can be
|
||||
used in client-side code as usual. At the end of testing, `mockServer.verify()` can be
|
||||
used to verify that all expectations have been satisfied.
|
||||
|
||||
By default, requests are expected in the order in which expectations were declared. You
|
||||
can set the `ignoreExpectOrder` option when building the server, in which case all
|
||||
expectations are checked (in order) to find a match for a given request. That means
|
||||
requests are allowed to come in any order. The following example uses `ignoreExpectOrder`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build()
|
||||
----
|
||||
|
||||
Even with unordered requests by default, each request is allowed to run once only.
|
||||
The `expect` method provides an overloaded variant that accepts an `ExpectedCount`
|
||||
argument that specifies a count range (for example, `once`, `manyTimes`, `max`, `min`,
|
||||
`between`, and so on). The following example uses `times`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();
|
||||
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess());
|
||||
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess());
|
||||
|
||||
// ...
|
||||
|
||||
mockServer.verify();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val restTemplate = RestTemplate()
|
||||
|
||||
val mockServer = MockRestServiceServer.bindTo(restTemplate).build()
|
||||
mockServer.expect(times(2), requestTo("/something")).andRespond(withSuccess())
|
||||
mockServer.expect(times(3), requestTo("/somewhere")).andRespond(withSuccess())
|
||||
|
||||
// ...
|
||||
|
||||
mockServer.verify()
|
||||
----
|
||||
|
||||
Note that, when `ignoreExpectOrder` is not set (the default), and, therefore, requests
|
||||
are expected in order of declaration, then that order applies only to the first of any
|
||||
expected request. For example if "/something" is expected two times followed by
|
||||
"/somewhere" three times, then there should be a request to "/something" before there is
|
||||
a request to "/somewhere", but, aside from that subsequent "/something" and "/somewhere",
|
||||
requests can come at any time.
|
||||
|
||||
As an alternative to all of the above, the client-side test support also provides a
|
||||
`ClientHttpRequestFactory` implementation that you can configure into a `RestTemplate` to
|
||||
bind it to a `MockMvc` instance. That allows processing requests using actual server-side
|
||||
logic but without running a server. The following example shows how to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
|
||||
this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));
|
||||
|
||||
// Test code that uses the above RestTemplate ...
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build()
|
||||
restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc))
|
||||
|
||||
// Test code that uses the above RestTemplate ...
|
||||
----
|
||||
|
||||
[[spring-mvc-test-client-static-imports]]
|
||||
== Static Imports
|
||||
|
||||
As with server-side tests, the fluent API for client-side tests requires a few static
|
||||
imports. Those are easy to find by searching for `MockRest*`. Eclipse users should add
|
||||
`MockRestRequestMatchers.{asterisk}` and `MockRestResponseCreators.{asterisk}` as
|
||||
"`favorite static members`" in the Eclipse preferences under Java -> Editor -> Content
|
||||
Assist -> Favorites. That allows using content assist after typing the first character of
|
||||
the static method name. Other IDEs (such IntelliJ) may not require any additional
|
||||
configuration. Check for the support for code completion on static members.
|
||||
|
||||
[[spring-mvc-test-client-resources]]
|
||||
== Further Examples of Client-side REST Tests
|
||||
|
||||
Spring MVC Test's own tests include
|
||||
{spring-framework-main-code}/spring-test/src/test/java/org/springframework/test/web/client/samples[example
|
||||
tests] of client-side REST tests.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
[[testing.appendix]]
|
||||
= Appendix
|
||||
|
||||
include::testing-annotations.adoc[leveloffset=+1]
|
||||
|
||||
include::testing-resources.adoc[leveloffset=+1]
|
||||
@@ -1,8 +0,0 @@
|
||||
[[testing-introduction]]
|
||||
= Introduction to Spring Testing
|
||||
|
||||
Testing is an integral part of enterprise software development. This chapter focuses on
|
||||
the value added by the IoC principle to <<unit-testing, unit testing>> and on the benefits
|
||||
of the Spring Framework's support for <<integration-testing, integration testing>>. (A
|
||||
thorough treatment of testing in the enterprise is beyond the scope of this reference
|
||||
manual.)
|
||||
@@ -1,32 +0,0 @@
|
||||
[[testing-resources]]
|
||||
= Further Resources
|
||||
See the following resources for more information about testing:
|
||||
|
||||
* https://www.junit.org/[JUnit]: "A programmer-friendly testing framework for Java and the JVM".
|
||||
Used by the Spring Framework in its test suite and supported in the
|
||||
<<testcontext-framework, Spring TestContext Framework>>.
|
||||
* https://testng.org/[TestNG]: A testing framework inspired by JUnit with added support
|
||||
for test groups, data-driven testing, distributed testing, and other features. Supported
|
||||
in the <<testcontext-framework, Spring TestContext Framework>>
|
||||
* https://assertj.github.io/doc/[AssertJ]: "Fluent assertions for Java",
|
||||
including support for Java 8 lambdas, streams, and numerous other features.
|
||||
* https://en.wikipedia.org/wiki/Mock_Object[Mock Objects]: Article in Wikipedia.
|
||||
* http://www.mockobjects.com/[MockObjects.com]: Web site dedicated to mock objects, a
|
||||
technique for improving the design of code within test-driven development.
|
||||
* https://mockito.github.io[Mockito]: Java mock library based on the
|
||||
http://xunitpatterns.com/Test%20Spy.html[Test Spy] pattern. Used by the Spring Framework
|
||||
in its test suite.
|
||||
* https://easymock.org/[EasyMock]: Java library "that provides Mock Objects for
|
||||
interfaces (and objects through the class extension) by generating them on the fly using
|
||||
Java's proxy mechanism."
|
||||
* https://jmock.org/[JMock]: Library that supports test-driven development of Java code
|
||||
with mock objects.
|
||||
* https://www.dbunit.org/[DbUnit]: JUnit extension (also usable with Ant and Maven) that
|
||||
is targeted at database-driven projects and, among other things, puts your database into
|
||||
a known state between test runs.
|
||||
* https://www.testcontainers.org/[Testcontainers]: Java library that supports JUnit
|
||||
tests, providing lightweight, throwaway instances of common databases, Selenium web
|
||||
browsers, or anything else that can run in a Docker container.
|
||||
* https://sourceforge.net/projects/grinder/[The Grinder]: Java load testing framework.
|
||||
* https://github.com/Ninja-Squad/springmockk[SpringMockK]: Support for Spring Boot
|
||||
integration tests written in Kotlin using https://mockk.io/[MockK] instead of Mockito.
|
||||
@@ -1,35 +0,0 @@
|
||||
[[integration-testing-support-jdbc]]
|
||||
= JDBC Testing Support
|
||||
|
||||
[[integration-testing-support-jdbc-test-utils]]
|
||||
== JdbcTestUtils
|
||||
|
||||
The `org.springframework.test.jdbc` package contains `JdbcTestUtils`, which is a
|
||||
collection of JDBC-related utility functions intended to simplify standard database
|
||||
testing scenarios. Specifically, `JdbcTestUtils` provides the following static utility
|
||||
methods.
|
||||
|
||||
* `countRowsInTable(..)`: Counts the number of rows in the given table.
|
||||
* `countRowsInTableWhere(..)`: Counts the number of rows in the given table by using the
|
||||
provided `WHERE` clause.
|
||||
* `deleteFromTables(..)`: Deletes all rows from the specified tables.
|
||||
* `deleteFromTableWhere(..)`: Deletes rows from the given table by using the provided
|
||||
`WHERE` clause.
|
||||
* `dropTables(..)`: Drops the specified tables.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
<<testcontext-support-classes-junit4, `AbstractTransactionalJUnit4SpringContextTests`>>
|
||||
and <<testcontext-support-classes-testng, `AbstractTransactionalTestNGSpringContextTests`>>
|
||||
provide convenience methods that delegate to the aforementioned methods in
|
||||
`JdbcTestUtils`.
|
||||
====
|
||||
|
||||
[[integration-testing-support-jdbc-embedded-database]]
|
||||
== Embedded Databases
|
||||
|
||||
The `spring-jdbc` module provides support for configuring and launching an embedded
|
||||
database, which you can use in integration tests that interact with a database.
|
||||
For details, see <<data-access.adoc#jdbc-embedded-database-support, Embedded Database
|
||||
Support>> and <<data-access.adoc#jdbc-embedded-database-dao-testing, Testing Data Access
|
||||
Logic with an Embedded Database>>.
|
||||
@@ -1,594 +0,0 @@
|
||||
[[webtestclient]]
|
||||
= WebTestClient
|
||||
|
||||
`WebTestClient` is an HTTP client designed for testing server applications. It wraps
|
||||
Spring's <<web-reactive.adoc#webflux-client, WebClient>> and uses it to perform requests
|
||||
but exposes a testing facade for verifying responses. `WebTestClient` can be used to
|
||||
perform end-to-end HTTP tests. It can also be used to test Spring MVC and Spring WebFlux
|
||||
applications without a running server via mock server request and response objects.
|
||||
|
||||
TIP: Kotlin users: See <<languages.adoc#kotlin-webtestclient-issue, this section>>
|
||||
related to use of the `WebTestClient`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webtestclient-setup]]
|
||||
== Setup
|
||||
|
||||
To set up a `WebTestClient` you need to choose a server setup to bind to. This can be one
|
||||
of several mock server setup choices or a connection to a live server.
|
||||
|
||||
|
||||
|
||||
[[webtestclient-controller-config]]
|
||||
=== Bind to Controller
|
||||
|
||||
This setup allows you to test specific controller(s) via mock request and response objects,
|
||||
without a running server.
|
||||
|
||||
For WebFlux applications, use the following which loads infrastructure equivalent to the
|
||||
<<web-reactive.adoc#webflux-config, WebFlux Java config>>, registers the given
|
||||
controller(s), and creates a <<web-reactive.adoc#webflux-web-handler-api, WebHandler chain>>
|
||||
to handle requests:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
WebTestClient client =
|
||||
WebTestClient.bindToController(new TestController()).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val client = WebTestClient.bindToController(TestController()).build()
|
||||
----
|
||||
|
||||
For Spring MVC, use the following which delegates to the
|
||||
{api-spring-framework}/test/web/servlet/setup/StandaloneMockMvcBuilder.html[StandaloneMockMvcBuilder]
|
||||
to load infrastructure equivalent to the <<web.adoc#mvc-config, WebMvc Java config>>,
|
||||
registers the given controller(s), and creates an instance of
|
||||
<<testing.adoc#spring-mvc-test-framework, MockMvc>> to handle requests:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
WebTestClient client =
|
||||
MockMvcWebTestClient.bindToController(new TestController()).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val client = MockMvcWebTestClient.bindToController(TestController()).build()
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[webtestclient-context-config]]
|
||||
=== Bind to `ApplicationContext`
|
||||
|
||||
This setup allows you to load Spring configuration with Spring MVC or Spring WebFlux
|
||||
infrastructure and controller declarations and use it to handle requests via mock request
|
||||
and response objects, without a running server.
|
||||
|
||||
For WebFlux, use the following where the Spring `ApplicationContext` is passed to
|
||||
{api-spring-framework}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder]
|
||||
to create the <<web-reactive.adoc#webflux-web-handler-api, WebHandler chain>> to handle
|
||||
requests:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@SpringJUnitConfig(WebConfig.class) // <1>
|
||||
class MyTests {
|
||||
|
||||
WebTestClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp(ApplicationContext context) { // <2>
|
||||
client = WebTestClient.bindToApplicationContext(context).build(); // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Specify the configuration to load
|
||||
<2> Inject the configuration
|
||||
<3> Create the `WebTestClient`
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@SpringJUnitConfig(WebConfig::class) // <1>
|
||||
class MyTests {
|
||||
|
||||
lateinit var client: WebTestClient
|
||||
|
||||
@BeforeEach
|
||||
fun setUp(context: ApplicationContext) { // <2>
|
||||
client = WebTestClient.bindToApplicationContext(context).build() // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Specify the configuration to load
|
||||
<2> Inject the configuration
|
||||
<3> Create the `WebTestClient`
|
||||
|
||||
For Spring MVC, use the following where the Spring `ApplicationContext` is passed to
|
||||
{api-spring-framework}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-[MockMvcBuilders.webAppContextSetup]
|
||||
to create a <<testing.adoc#spring-mvc-test-framework, MockMvc>> instance to handle
|
||||
requests:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration("classpath:META-INF/web-resources") // <1>
|
||||
@ContextHierarchy({
|
||||
@ContextConfiguration(classes = RootConfig.class),
|
||||
@ContextConfiguration(classes = WebConfig.class)
|
||||
})
|
||||
class MyTests {
|
||||
|
||||
@Autowired
|
||||
WebApplicationContext wac; // <2>
|
||||
|
||||
WebTestClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
client = MockMvcWebTestClient.bindToApplicationContext(this.wac).build(); // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Specify the configuration to load
|
||||
<2> Inject the configuration
|
||||
<3> Create the `WebTestClient`
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration("classpath:META-INF/web-resources") // <1>
|
||||
@ContextHierarchy({
|
||||
@ContextConfiguration(classes = RootConfig.class),
|
||||
@ContextConfiguration(classes = WebConfig.class)
|
||||
})
|
||||
class MyTests {
|
||||
|
||||
@Autowired
|
||||
lateinit var wac: WebApplicationContext; // <2>
|
||||
|
||||
lateinit var client: WebTestClient
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() { // <2>
|
||||
client = MockMvcWebTestClient.bindToApplicationContext(wac).build() // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Specify the configuration to load
|
||||
<2> Inject the configuration
|
||||
<3> Create the `WebTestClient`
|
||||
|
||||
|
||||
|
||||
[[webtestclient-fn-config]]
|
||||
=== Bind to Router Function
|
||||
|
||||
This setup allows you to test <<web-reactive.adoc#webflux-fn, functional endpoints>> via
|
||||
mock request and response objects, without a running server.
|
||||
|
||||
For WebFlux, use the following which delegates to `RouterFunctions.toWebHandler` to
|
||||
create a server setup to handle requests:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<?> route = ...
|
||||
client = WebTestClient.bindToRouterFunction(route).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val route: RouterFunction<*> = ...
|
||||
val client = WebTestClient.bindToRouterFunction(route).build()
|
||||
----
|
||||
|
||||
For Spring MVC there are currently no options to test
|
||||
<<web.adoc#webmvc-fn, WebMvc functional endpoints>>.
|
||||
|
||||
|
||||
|
||||
[[webtestclient-server-config]]
|
||||
=== Bind to Server
|
||||
|
||||
This setup connects to a running server to perform full, end-to-end HTTP tests:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[webtestclient-client-config]]
|
||||
=== Client Config
|
||||
|
||||
In addition to the server setup options described earlier, you can also configure client
|
||||
options, including base URL, default headers, client filters, and others. These options
|
||||
are readily available following `bindToServer()`. For all other configuration options,
|
||||
you need to use `configureClient()` to transition from server to client configuration, as
|
||||
follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
client = WebTestClient.bindToController(new TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
client = WebTestClient.bindToController(TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.build()
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[webtestclient-tests]]
|
||||
== Writing Tests
|
||||
|
||||
`WebTestClient` provides an API identical to <<web-reactive.adoc#webflux-client, WebClient>>
|
||||
up to the point of performing a request by using `exchange()`. See the
|
||||
<<web-reactive.adoc#webflux-client-body, WebClient>> documentation for examples on how to
|
||||
prepare a request with any content including form data, multipart data, and more.
|
||||
|
||||
After the call to `exchange()`, `WebTestClient` diverges from the `WebClient` and
|
||||
instead continues with a workflow to verify responses.
|
||||
|
||||
To assert the response status and headers, use the following:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
----
|
||||
|
||||
If you would like for all expectations to be asserted even if one of them fails, you can
|
||||
use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is
|
||||
similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in
|
||||
JUnit Jupiter.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange()
|
||||
.expectAll(
|
||||
spec -> spec.expectStatus().isOk(),
|
||||
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||
);
|
||||
----
|
||||
|
||||
You can then choose to decode the response body through one of the following:
|
||||
|
||||
* `expectBody(Class<T>)`: Decode to single object.
|
||||
* `expectBodyList(Class<T>)`: Decode and collect objects to `List<T>`.
|
||||
* `expectBody()`: Decode to `byte[]` for <<webtestclient-json>> or an empty body.
|
||||
|
||||
And perform assertions on the resulting higher level Object(s):
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBodyList(Person.class).hasSize(3).contains(person);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.test.web.reactive.server.expectBodyList
|
||||
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBodyList<Person>().hasSize(3).contains(person)
|
||||
----
|
||||
|
||||
If the built-in assertions are insufficient, you can consume the object instead and
|
||||
perform any other assertions:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import org.springframework.test.web.reactive.server.expectBody
|
||||
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.consumeWith(result -> {
|
||||
// custom assertions (e.g. AssertJ)...
|
||||
});
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody<Person>()
|
||||
.consumeWith {
|
||||
// custom assertions (e.g. AssertJ)...
|
||||
}
|
||||
----
|
||||
|
||||
Or you can exit the workflow and obtain an `EntityExchangeResult`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.test.web.reactive.server.expectBody
|
||||
|
||||
val result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk
|
||||
.expectBody<Person>()
|
||||
.returnResult()
|
||||
----
|
||||
|
||||
TIP: When you need to decode to a target type with generics, look for the overloaded methods
|
||||
that accept
|
||||
{api-spring-framework}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`]
|
||||
instead of `Class<T>`.
|
||||
|
||||
|
||||
|
||||
[[webtestclient-no-content]]
|
||||
=== No Content
|
||||
|
||||
If the response is not expected to have content, you can assert that as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
client.post().uri("/persons")
|
||||
.body(personMono, Person.class)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody().isEmpty();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
client.post().uri("/persons")
|
||||
.bodyValue(person)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody().isEmpty()
|
||||
----
|
||||
|
||||
If you want to ignore the response content, the following releases the content without
|
||||
any assertions:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound()
|
||||
.expectBody(Void.class);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound
|
||||
.expectBody<Unit>()
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[webtestclient-json]]
|
||||
=== JSON Content
|
||||
|
||||
You can use `expectBody()` without a target type to perform assertions on the raw
|
||||
content rather than through higher level Object(s).
|
||||
|
||||
To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.json("{\"name\":\"Jane\"}")
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.json("{\"name\":\"Jane\"}")
|
||||
----
|
||||
|
||||
To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$[0].name").isEqualTo("Jane")
|
||||
.jsonPath("$[1].name").isEqualTo("Jason");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
client.get().uri("/persons")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.jsonPath("$[0].name").isEqualTo("Jane")
|
||||
.jsonPath("$[1].name").isEqualTo("Jason")
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[webtestclient-stream]]
|
||||
=== Streaming Responses
|
||||
|
||||
To test potentially infinite streams such as `"text/event-stream"` or
|
||||
`"application/x-ndjson"`, start by verifying the response status and headers, and then
|
||||
obtain a `FluxExchangeResult`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
FluxExchangeResult<MyEvent> result = client.get().uri("/events")
|
||||
.accept(TEXT_EVENT_STREAM)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.returnResult(MyEvent.class);
|
||||
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.test.web.reactive.server.returnResult
|
||||
|
||||
val result = client.get().uri("/events")
|
||||
.accept(TEXT_EVENT_STREAM)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.returnResult<MyEvent>()
|
||||
----
|
||||
|
||||
Now you're ready to consume the response stream with `StepVerifier` from `reactor-test`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
Flux<Event> eventFlux = result.getResponseBody();
|
||||
|
||||
StepVerifier.create(eventFlux)
|
||||
.expectNext(person)
|
||||
.expectNextCount(4)
|
||||
.consumeNextWith(p -> ...)
|
||||
.thenCancel()
|
||||
.verify();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val eventFlux = result.getResponseBody()
|
||||
|
||||
StepVerifier.create(eventFlux)
|
||||
.expectNext(person)
|
||||
.expectNextCount(4)
|
||||
.consumeNextWith { p -> ... }
|
||||
.thenCancel()
|
||||
.verify()
|
||||
----
|
||||
|
||||
|
||||
[[webtestclient-mockmvc]]
|
||||
=== MockMvc Assertions
|
||||
|
||||
`WebTestClient` is an HTTP client and as such it can only verify what is in the client
|
||||
response including status, headers, and body.
|
||||
|
||||
When testing a Spring MVC application with a MockMvc server setup, you have the extra
|
||||
choice to perform further assertions on the server response. To do that start by
|
||||
obtaining an `ExchangeResult` after asserting the body:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
// For a response with a body
|
||||
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
|
||||
// For a response without a body
|
||||
EntityExchangeResult<Void> result = client.get().uri("/path")
|
||||
.exchange()
|
||||
.expectBody().isEmpty();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
// For a response with a body
|
||||
val result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
|
||||
// For a response without a body
|
||||
val result = client.get().uri("/path")
|
||||
.exchange()
|
||||
.expectBody().isEmpty();
|
||||
----
|
||||
|
||||
Then switch to MockMvc server response assertions:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
MockMvcWebTestClient.resultActionsFor(result)
|
||||
.andExpect(model().attribute("integer", 3))
|
||||
.andExpect(model().attribute("string", "a string value"));
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
MockMvcWebTestClient.resultActionsFor(result)
|
||||
.andExpect(model().attribute("integer", 3))
|
||||
.andExpect(model().attribute("string", "a string value"));
|
||||
----
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
[[unit-testing]]
|
||||
= Unit Testing
|
||||
|
||||
Dependency injection should make your code less dependent on the container than it would
|
||||
be with traditional J2EE / Java EE development. The POJOs that make up your application
|
||||
should be testable in JUnit or TestNG tests, with objects instantiated by using the `new`
|
||||
operator, without Spring or any other container. You can use <<mock-objects, mock objects>>
|
||||
(in conjunction with other valuable testing techniques) to test your code in isolation.
|
||||
If you follow the architecture recommendations for Spring, the resulting clean layering
|
||||
and componentization of your codebase facilitate easier unit testing. For example,
|
||||
you can test service layer objects by stubbing or mocking DAO or repository interfaces,
|
||||
without needing to access persistent data while running unit tests.
|
||||
|
||||
True unit tests typically run extremely quickly, as there is no runtime infrastructure to
|
||||
set up. Emphasizing true unit tests as part of your development methodology can boost
|
||||
your productivity. You may not need this section of the testing chapter to help you write
|
||||
effective unit tests for your IoC-based applications. For certain unit testing scenarios,
|
||||
however, the Spring Framework provides mock objects and testing support classes, which
|
||||
are described in this chapter.
|
||||
|
||||
|
||||
|
||||
[[mock-objects]]
|
||||
== Mock Objects
|
||||
|
||||
Spring includes a number of packages dedicated to mocking:
|
||||
|
||||
* <<mock-objects-env>>
|
||||
* <<mock-objects-jndi>>
|
||||
* <<mock-objects-servlet>>
|
||||
* <<mock-objects-web-reactive>>
|
||||
|
||||
|
||||
[[mock-objects-env]]
|
||||
=== Environment
|
||||
|
||||
The `org.springframework.mock.env` package contains mock implementations of the
|
||||
`Environment` and `PropertySource` abstractions (see
|
||||
<<core.adoc#beans-definition-profiles, Bean Definition Profiles>>
|
||||
and <<core.adoc#beans-property-source-abstraction, `PropertySource` Abstraction>>).
|
||||
`MockEnvironment` and `MockPropertySource` are useful for developing
|
||||
out-of-container tests for code that depends on environment-specific properties.
|
||||
|
||||
|
||||
[[mock-objects-jndi]]
|
||||
=== JNDI
|
||||
|
||||
The `org.springframework.mock.jndi` package contains a partial implementation of the JNDI
|
||||
SPI, which you can use to set up a simple JNDI environment for test suites or stand-alone
|
||||
applications. If, for example, JDBC `DataSource` instances get bound to the same JNDI
|
||||
names in test code as they do in a Jakarta EE container, you can reuse both application code
|
||||
and configuration in testing scenarios without modification.
|
||||
|
||||
WARNING: The mock JNDI support in the `org.springframework.mock.jndi` package is
|
||||
officially deprecated as of Spring Framework 5.2 in favor of complete solutions from third
|
||||
parties such as https://github.com/h-thurow/Simple-JNDI[Simple-JNDI].
|
||||
|
||||
|
||||
[[mock-objects-servlet]]
|
||||
=== Servlet API
|
||||
|
||||
The `org.springframework.mock.web` package contains a comprehensive set of Servlet API
|
||||
mock objects that are useful for testing web contexts, controllers, and filters. These
|
||||
mock objects are targeted at usage with Spring's Web MVC framework and are generally more
|
||||
convenient to use than dynamic mock objects (such as https://easymock.org/[EasyMock])
|
||||
or alternative Servlet API mock objects (such as http://www.mockobjects.com[MockObjects]).
|
||||
|
||||
TIP: Since Spring Framework 6.0, the mock objects in `org.springframework.mock.web` are
|
||||
based on the Servlet 6.0 API.
|
||||
|
||||
The Spring MVC Test framework builds on the mock Servlet API objects to provide an
|
||||
integration testing framework for Spring MVC. See <<spring-mvc-test-framework>>.
|
||||
|
||||
|
||||
[[mock-objects-web-reactive]]
|
||||
=== Spring Web Reactive
|
||||
|
||||
The `org.springframework.mock.http.server.reactive` package contains mock implementations
|
||||
of `ServerHttpRequest` and `ServerHttpResponse` for use in WebFlux applications. The
|
||||
`org.springframework.mock.web.server` package contains a mock `ServerWebExchange` that
|
||||
depends on those mock request and response objects.
|
||||
|
||||
Both `MockServerHttpRequest` and `MockServerHttpResponse` extend from the same abstract
|
||||
base classes as server-specific implementations and share behavior with them. For
|
||||
example, a mock request is immutable once created, but you can use the `mutate()` method
|
||||
from `ServerHttpRequest` to create a modified instance.
|
||||
|
||||
In order for the mock response to properly implement the write contract and return a
|
||||
write completion handle (that is, `Mono<Void>`), it by default uses a `Flux` with
|
||||
`cache().then()`, which buffers the data and makes it available for assertions in tests.
|
||||
Applications can set a custom write function (for example, to test an infinite stream).
|
||||
|
||||
The <<webtestclient>> builds on the mock request and response to provide support for
|
||||
testing WebFlux applications without an HTTP server. The client can also be used for
|
||||
end-to-end tests with a running server.
|
||||
|
||||
|
||||
|
||||
[[unit-testing-support-classes]]
|
||||
== Unit Testing Support Classes
|
||||
|
||||
Spring includes a number of classes that can help with unit testing. They fall into two
|
||||
categories:
|
||||
|
||||
* <<unit-testing-utilities>>
|
||||
* <<unit-testing-spring-mvc>>
|
||||
|
||||
|
||||
[[unit-testing-utilities]]
|
||||
=== General Testing Utilities
|
||||
|
||||
The `org.springframework.test.util` package contains several general purpose utilities
|
||||
for use in unit and integration testing.
|
||||
|
||||
{api-spring-framework}/test/util/AopTestUtils.html[`AopTestUtils`] is a collection of
|
||||
AOP-related utility methods. You can use these methods to obtain a reference to the
|
||||
underlying target object hidden behind one or more Spring proxies. For example, if you
|
||||
have configured a bean as a dynamic mock by using a library such as EasyMock or Mockito,
|
||||
and the mock is wrapped in a Spring proxy, you may need direct access to the underlying
|
||||
mock to configure expectations on it and perform verifications. For Spring's core AOP
|
||||
utilities, see {api-spring-framework}/aop/support/AopUtils.html[`AopUtils`] and
|
||||
{api-spring-framework}/aop/framework/AopProxyUtils.html[`AopProxyUtils`].
|
||||
|
||||
{api-spring-framework}/test/util/ReflectionTestUtils.html[`ReflectionTestUtils`] is a
|
||||
collection of reflection-based utility methods. You can use these methods in testing
|
||||
scenarios where you need to change the value of a constant, set a non-`public` field,
|
||||
invoke a non-`public` setter method, or invoke a non-`public` configuration or lifecycle
|
||||
callback method when testing application code for use cases such as the following:
|
||||
|
||||
* ORM frameworks (such as JPA and Hibernate) that condone `private` or `protected` field
|
||||
access as opposed to `public` setter methods for properties in a domain entity.
|
||||
* Spring's support for annotations (such as `@Autowired`, `@Inject`, and `@Resource`),
|
||||
that provide dependency injection for `private` or `protected` fields, setter methods,
|
||||
and configuration methods.
|
||||
* Use of annotations such as `@PostConstruct` and `@PreDestroy` for lifecycle callback
|
||||
methods.
|
||||
|
||||
{api-spring-framework}/test/util/TestSocketUtils.html[`TestSocketUtils`] is a simple
|
||||
utility for finding available TCP ports on `localhost` for use in integration testing
|
||||
scenarios.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
`TestSocketUtils` can be used in integration tests which start an external server on an
|
||||
available random port. However, these utilities make no guarantee about the subsequent
|
||||
availability of a given port and are therefore unreliable. Instead of using
|
||||
`TestSocketUtils` to find an available local port for a server, it is recommended that
|
||||
you rely on a server's ability to start on a random ephemeral port that it selects or is
|
||||
assigned by the operating system. To interact with that server, you should query the
|
||||
server for the port it is currently using.
|
||||
====
|
||||
|
||||
|
||||
[[unit-testing-spring-mvc]]
|
||||
=== Spring MVC Testing Utilities
|
||||
|
||||
The `org.springframework.test.web` package contains
|
||||
{api-spring-framework}/test/web/ModelAndViewAssert.html[`ModelAndViewAssert`], which you
|
||||
can use in combination with JUnit, TestNG, or any other testing framework for unit tests
|
||||
that deal with Spring MVC `ModelAndView` objects.
|
||||
|
||||
.Unit testing Spring MVC Controllers
|
||||
TIP: To unit test your Spring MVC `Controller` classes as POJOs, use `ModelAndViewAssert`
|
||||
combined with `MockHttpServletRequest`, `MockHttpSession`, and so on from Spring's
|
||||
<<mock-objects-servlet, Servlet API mocks>>. For thorough integration testing of your
|
||||
Spring MVC and REST `Controller` classes in conjunction with your `WebApplicationContext`
|
||||
configuration for Spring MVC, use the
|
||||
<<spring-mvc-test-framework, Spring MVC Test Framework>> instead.
|
||||
@@ -1,199 +0,0 @@
|
||||
[[web-integration]]
|
||||
= Other Web Frameworks
|
||||
|
||||
This chapter details Spring's integration with third-party web frameworks.
|
||||
|
||||
One of the core value propositions of the Spring Framework is that of enabling
|
||||
_choice_. In a general sense, Spring does not force you to use or buy into any
|
||||
particular architecture, technology, or methodology (although it certainly recommends
|
||||
some over others). This freedom to pick and choose the architecture, technology, or
|
||||
methodology that is most relevant to a developer and their development team is
|
||||
arguably most evident in the web area, where Spring provides its own web frameworks
|
||||
(<<mvc, Spring MVC>> and <<web-reactive.adoc#webflux, Spring WebFlux>>) while, at the same time,
|
||||
supporting integration with a number of popular third-party web frameworks.
|
||||
|
||||
|
||||
|
||||
|
||||
[[web-integration-common]]
|
||||
== Common Configuration
|
||||
|
||||
Before diving into the integration specifics of each supported web framework, let us
|
||||
first take a look at common Spring configuration that is not specific to any one web
|
||||
framework. (This section is equally applicable to Spring's own web framework variants.)
|
||||
|
||||
One of the concepts (for want of a better word) espoused by Spring's lightweight
|
||||
application model is that of a layered architecture. Remember that in a "classic"
|
||||
layered architecture, the web layer is but one of many layers. It serves as one of the
|
||||
entry points into a server-side application, and it delegates to service objects
|
||||
(facades) that are defined in a service layer to satisfy business-specific (and
|
||||
presentation-technology agnostic) use cases. In Spring, these service objects, any other
|
||||
business-specific objects, data-access objects, and others exist in a distinct "business
|
||||
context", which contains no web or presentation layer objects (presentation objects,
|
||||
such as Spring MVC controllers, are typically configured in a distinct "presentation
|
||||
context"). This section details how you can configure a Spring container (a
|
||||
`WebApplicationContext`) that contains all of the 'business beans' in your application.
|
||||
|
||||
Moving on to specifics, all you need to do is declare a
|
||||
{api-spring-framework}/web/context/ContextLoaderListener.html[`ContextLoaderListener`]
|
||||
in the standard Jakarta EE servlet `web.xml` file of your web application and add a
|
||||
`contextConfigLocation` `<context-param/>` section (in the same file) that defines which
|
||||
set of Spring XML configuration files to load.
|
||||
|
||||
Consider the following `<listener/>` configuration:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<listener>
|
||||
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
|
||||
</listener>
|
||||
----
|
||||
|
||||
Further consider the following `<context-param/>` configuration:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<context-param>
|
||||
<param-name>contextConfigLocation</param-name>
|
||||
<param-value>/WEB-INF/applicationContext*.xml</param-value>
|
||||
</context-param>
|
||||
----
|
||||
|
||||
If you do not specify the `contextConfigLocation` context parameter, the
|
||||
`ContextLoaderListener` looks for a file called `/WEB-INF/applicationContext.xml` to
|
||||
load. Once the context files are loaded, Spring creates a
|
||||
{api-spring-framework}/web/context/WebApplicationContext.html[`WebApplicationContext`]
|
||||
object based on the bean definitions and stores it in the `ServletContext` of the web
|
||||
application.
|
||||
|
||||
All Java web frameworks are built on top of the Servlet API, so you can use the
|
||||
following code snippet to get access to this "business context" `ApplicationContext`
|
||||
created by the `ContextLoaderListener`.
|
||||
|
||||
The following example shows how to get the `WebApplicationContext`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
|
||||
----
|
||||
|
||||
The
|
||||
{api-spring-framework}/web/context/support/WebApplicationContextUtils.html[`WebApplicationContextUtils`]
|
||||
class is for convenience, so you need not remember the name of the `ServletContext`
|
||||
attribute. Its `getWebApplicationContext()` method returns `null` if an object
|
||||
does not exist under the `WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`
|
||||
key. Rather than risk getting `NullPointerExceptions` in your application, it is better
|
||||
to use the `getRequiredWebApplicationContext()` method. This method throws an exception
|
||||
when the `ApplicationContext` is missing.
|
||||
|
||||
Once you have a reference to the `WebApplicationContext`, you can retrieve beans by their
|
||||
name or type. Most developers retrieve beans by name and then cast them to one of their
|
||||
implemented interfaces.
|
||||
|
||||
Fortunately, most of the frameworks in this section have simpler ways of looking up beans.
|
||||
Not only do they make it easy to get beans from a Spring container, but they also let you
|
||||
use dependency injection on their controllers. Each web framework section has more detail
|
||||
on its specific integration strategies.
|
||||
|
||||
|
||||
|
||||
|
||||
[[jsf]]
|
||||
== JSF
|
||||
|
||||
JavaServer Faces (JSF) is the JCP's standard component-based, event-driven web
|
||||
user interface framework. It is an official part of the Jakarta EE umbrella but also
|
||||
individually usable, e.g. through embedding Mojarra or MyFaces within Tomcat.
|
||||
|
||||
Please note that recent versions of JSF became closely tied to CDI infrastructure
|
||||
in application servers, with some new JSF functionality only working in such an
|
||||
environment. Spring's JSF support is not actively evolved anymore and primarily
|
||||
exists for migration purposes when modernizing older JSF-based applications.
|
||||
|
||||
The key element in Spring's JSF integration is the JSF `ELResolver` mechanism.
|
||||
|
||||
|
||||
|
||||
[[jsf-springbeanfaceselresolver]]
|
||||
=== Spring Bean Resolver
|
||||
|
||||
`SpringBeanFacesELResolver` is a JSF compliant `ELResolver` implementation,
|
||||
integrating with the standard Unified EL as used by JSF and JSP. It delegates to
|
||||
Spring's "business context" `WebApplicationContext` first and then to the
|
||||
default resolver of the underlying JSF implementation.
|
||||
|
||||
Configuration-wise, you can define `SpringBeanFacesELResolver` in your JSF
|
||||
`faces-context.xml` file, as the following example shows:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<faces-config>
|
||||
<application>
|
||||
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
|
||||
...
|
||||
</application>
|
||||
</faces-config>
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[jsf-facescontextutils]]
|
||||
=== Using `FacesContextUtils`
|
||||
|
||||
A custom `ELResolver` works well when mapping your properties to beans in
|
||||
`faces-config.xml`, but, at times, you may need to explicitly grab a bean.
|
||||
The {api-spring-framework}/web/jsf/FacesContextUtils.html[`FacesContextUtils`]
|
||||
class makes this easy. It is similar to `WebApplicationContextUtils`, except that
|
||||
it takes a `FacesContext` parameter rather than a `ServletContext` parameter.
|
||||
|
||||
The following example shows how to use `FacesContextUtils`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[struts]]
|
||||
== Apache Struts
|
||||
|
||||
Invented by Craig McClanahan, https://struts.apache.org[Struts] is an open-source project
|
||||
hosted by the Apache Software Foundation. Struts 1.x greatly simplified the
|
||||
JSP/Servlet programming paradigm and won over many developers who were using proprietary
|
||||
frameworks. It simplified the programming model; it was open source; and it had a large
|
||||
community, which let the project grow and become popular among Java web developers.
|
||||
|
||||
As a successor to the original Struts 1.x, check out Struts 2.x or more recent versions
|
||||
as well as the Struts-provided
|
||||
https://struts.apache.org/plugins/spring/[Spring Plugin] for built-in Spring integration.
|
||||
|
||||
|
||||
|
||||
|
||||
[[tapestry]]
|
||||
== Apache Tapestry
|
||||
|
||||
https://tapestry.apache.org/[Tapestry] is a "Component oriented framework for creating
|
||||
dynamic, robust, highly scalable web applications in Java."
|
||||
|
||||
While Spring has its own <<mvc, powerful web layer>>, there are a number of unique
|
||||
advantages to building an enterprise Java application by using a combination of Tapestry
|
||||
for the web user interface and the Spring container for the lower layers.
|
||||
|
||||
For more information, see Tapestry's dedicated
|
||||
https://tapestry.apache.org/integrating-with-spring-framework.html[integration module for Spring].
|
||||
|
||||
|
||||
|
||||
|
||||
[[web-integration-resources]]
|
||||
== Further Resources
|
||||
|
||||
The following links go to further resources about the various web frameworks described in
|
||||
this chapter.
|
||||
|
||||
* The https://www.oracle.com/java/technologies/javaserverfaces.html[JSF] homepage
|
||||
* The https://struts.apache.org/[Struts] homepage
|
||||
* The https://tapestry.apache.org/[Tapestry] homepage
|
||||
@@ -1,95 +0,0 @@
|
||||
In the context of web applications, _data binding_ involves the binding of HTTP request
|
||||
parameters (that is, form data or query parameters) to properties in a model object and
|
||||
its nested objects.
|
||||
|
||||
Only `public` properties following the
|
||||
https://www.oracle.com/java/technologies/javase/javabeans-spec.html[JavaBeans naming conventions]
|
||||
are exposed for data binding — for example, `public String getFirstName()` and
|
||||
`public void setFirstName(String)` methods for a `firstName` property.
|
||||
|
||||
TIP: The model object, and its nested object graph, is also sometimes referred to as a
|
||||
_command object_, _form-backing object_, or _POJO_ (Plain Old Java Object).
|
||||
|
||||
By default, Spring permits binding to all public properties in the model object graph.
|
||||
This means you need to carefully consider what public properties the model has, since a
|
||||
client could target any public property path, even some that are not expected to be
|
||||
targeted for a given use case.
|
||||
|
||||
For example, given an HTTP form data endpoint, a malicious client could supply values for
|
||||
properties that exist in the model object graph but are not part of the HTML form
|
||||
presented in the browser. This could lead to data being set on the model object and any
|
||||
of its nested objects, that is not expected to be updated.
|
||||
|
||||
The recommended approach is to use a _dedicated model object_ that exposes only
|
||||
properties that are relevant for the form submission. For example, on a form for changing
|
||||
a user's email address, the model object should declare a minimum set of properties such
|
||||
as in the following `ChangeEmailForm`.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public class ChangeEmailForm {
|
||||
|
||||
private String oldEmailAddress;
|
||||
private String newEmailAddress;
|
||||
|
||||
public void setOldEmailAddress(String oldEmailAddress) {
|
||||
this.oldEmailAddress = oldEmailAddress;
|
||||
}
|
||||
|
||||
public String getOldEmailAddress() {
|
||||
return this.oldEmailAddress;
|
||||
}
|
||||
|
||||
public void setNewEmailAddress(String newEmailAddress) {
|
||||
this.newEmailAddress = newEmailAddress;
|
||||
}
|
||||
|
||||
public String getNewEmailAddress() {
|
||||
return this.newEmailAddress;
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
If you cannot or do not want to use a _dedicated model object_ for each data
|
||||
binding use case, you **must** limit the properties that are allowed for data binding.
|
||||
Ideally, you can achieve this by registering _allowed field patterns_ via the
|
||||
`setAllowedFields()` method on `WebDataBinder`.
|
||||
|
||||
For example, to register allowed field patterns in your application, you can implement an
|
||||
`@InitBinder` method in a `@Controller` or `@ControllerAdvice` component as shown below:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Controller
|
||||
public class ChangeEmailController {
|
||||
|
||||
@InitBinder
|
||||
void initBinder(WebDataBinder binder) {
|
||||
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
|
||||
}
|
||||
|
||||
// @RequestMapping methods, etc.
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
In addition to registering allowed patterns, it is also possible to register _disallowed
|
||||
field patterns_ via the `setDisallowedFields()` method in `DataBinder` and its subclasses.
|
||||
Please note, however, that an "allow list" is safer than a "deny list". Consequently,
|
||||
`setAllowedFields()` should be favored over `setDisallowedFields()`.
|
||||
|
||||
Note that matching against allowed field patterns is case-sensitive; whereas, matching
|
||||
against disallowed field patterns is case-insensitive. In addition, a field matching a
|
||||
disallowed pattern will not be accepted even if it also happens to match a pattern in the
|
||||
allowed list.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
It is extremely important to properly configure allowed and disallowed field patterns
|
||||
when exposing your domain model directly for data binding purposes. Otherwise, it is a
|
||||
big security risk.
|
||||
|
||||
Furthermore, it is strongly recommended that you do **not** use types from your domain
|
||||
model such as JPA or Hibernate entities as the model object in data binding scenarios.
|
||||
====
|
||||
@@ -1,330 +0,0 @@
|
||||
[id={chapter}.web-uricomponents]
|
||||
= UriComponents
|
||||
[.small]#Spring MVC and Spring WebFlux#
|
||||
|
||||
`UriComponentsBuilder` helps to build URI's from URI templates with variables, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
UriComponents uriComponents = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}") // <1>
|
||||
.queryParam("q", "{q}") // <2>
|
||||
.encode() // <3>
|
||||
.build(); // <4>
|
||||
|
||||
URI uri = uriComponents.expand("Westin", "123").toUri(); // <5>
|
||||
----
|
||||
<1> Static factory method with a URI template.
|
||||
<2> Add or replace URI components.
|
||||
<3> Request to have the URI template and URI variables encoded.
|
||||
<4> Build a `UriComponents`.
|
||||
<5> Expand variables and obtain the `URI`.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uriComponents = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}") // <1>
|
||||
.queryParam("q", "{q}") // <2>
|
||||
.encode() // <3>
|
||||
.build() // <4>
|
||||
|
||||
val uri = uriComponents.expand("Westin", "123").toUri() // <5>
|
||||
----
|
||||
<1> Static factory method with a URI template.
|
||||
<2> Add or replace URI components.
|
||||
<3> Request to have the URI template and URI variables encoded.
|
||||
<4> Build a `UriComponents`.
|
||||
<5> Expand variables and obtain the `URI`.
|
||||
|
||||
The preceding example can be consolidated into one chain and shortened with `buildAndExpand`,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.encode()
|
||||
.buildAndExpand("Westin", "123")
|
||||
.toUri();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.encode()
|
||||
.buildAndExpand("Westin", "123")
|
||||
.toUri()
|
||||
----
|
||||
|
||||
You can shorten it further by going directly to a URI (which implies encoding),
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("Westin", "123");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("Westin", "123")
|
||||
----
|
||||
|
||||
You can shorten it further still with a full URI template, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
|
||||
.build("Westin", "123");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder
|
||||
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
|
||||
.build("Westin", "123")
|
||||
----
|
||||
|
||||
|
||||
|
||||
[id={chapter}.web-uribuilder]
|
||||
= UriBuilder
|
||||
[.small]#Spring MVC and Spring WebFlux#
|
||||
|
||||
<<{chapter}.web-uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a
|
||||
`UriBuilder`, in turn, with a `UriBuilderFactory`. Together, `UriBuilderFactory` and
|
||||
`UriBuilder` provide a pluggable mechanism to build URIs from URI templates, based on
|
||||
shared configuration, such as a base URL, encoding preferences, and other details.
|
||||
|
||||
You can configure `RestTemplate` and `WebClient` with a `UriBuilderFactory`
|
||||
to customize the preparation of URIs. `DefaultUriBuilderFactory` is a default
|
||||
implementation of `UriBuilderFactory` that uses `UriComponentsBuilder` internally and
|
||||
exposes shared configuration options.
|
||||
|
||||
The following example shows how to configure a `RestTemplate`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
|
||||
|
||||
String baseUrl = "https://example.org";
|
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
|
||||
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(factory);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
|
||||
|
||||
val baseUrl = "https://example.org"
|
||||
val factory = DefaultUriBuilderFactory(baseUrl)
|
||||
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
|
||||
|
||||
val restTemplate = RestTemplate()
|
||||
restTemplate.uriTemplateHandler = factory
|
||||
----
|
||||
|
||||
The following example configures a `WebClient`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
|
||||
|
||||
String baseUrl = "https://example.org";
|
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
|
||||
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
|
||||
|
||||
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
|
||||
|
||||
val baseUrl = "https://example.org"
|
||||
val factory = DefaultUriBuilderFactory(baseUrl)
|
||||
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
|
||||
|
||||
val client = WebClient.builder().uriBuilderFactory(factory).build()
|
||||
----
|
||||
|
||||
In addition, you can also use `DefaultUriBuilderFactory` directly. It is similar to using
|
||||
`UriComponentsBuilder` but, instead of static factory methods, it is an actual instance
|
||||
that holds configuration and preferences, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
String baseUrl = "https://example.com";
|
||||
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
|
||||
|
||||
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("Westin", "123");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val baseUrl = "https://example.com"
|
||||
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)
|
||||
|
||||
val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("Westin", "123")
|
||||
----
|
||||
|
||||
|
||||
[id={chapter}.web-uri-encoding]
|
||||
= URI Encoding
|
||||
[.small]#Spring MVC and Spring WebFlux#
|
||||
|
||||
`UriComponentsBuilder` exposes encoding options at two levels:
|
||||
|
||||
* {api-spring-framework}/web/util/UriComponentsBuilder.html#encode--[UriComponentsBuilder#encode()]:
|
||||
Pre-encodes the URI template first and then strictly encodes URI variables when expanded.
|
||||
* {api-spring-framework}/web/util/UriComponents.html#encode--[UriComponents#encode()]:
|
||||
Encodes URI components _after_ URI variables are expanded.
|
||||
|
||||
Both options replace non-ASCII and illegal characters with escaped octets. However, the first option
|
||||
also replaces characters with reserved meaning that appear in URI variables.
|
||||
|
||||
TIP: Consider ";", which is legal in a path but has reserved meaning. The first option replaces
|
||||
";" with "%3B" in URI variables but not in the URI template. By contrast, the second option never
|
||||
replaces ";", since it is a legal character in a path.
|
||||
|
||||
For most cases, the first option is likely to give the expected result, because it treats URI
|
||||
variables as opaque data to be fully encoded, while the second option is useful if URI
|
||||
variables do intentionally contain reserved characters. The second option is also useful
|
||||
when not expanding URI variables at all since that will also encode anything that
|
||||
incidentally looks like a URI variable.
|
||||
|
||||
The following example uses the first option:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
|
||||
.queryParam("q", "{q}")
|
||||
.encode()
|
||||
.buildAndExpand("New York", "foo+bar")
|
||||
.toUri();
|
||||
|
||||
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
|
||||
.queryParam("q", "{q}")
|
||||
.encode()
|
||||
.buildAndExpand("New York", "foo+bar")
|
||||
.toUri()
|
||||
|
||||
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
|
||||
----
|
||||
|
||||
You can shorten the preceding example by going directly to the URI (which implies encoding),
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("New York", "foo+bar");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
|
||||
.queryParam("q", "{q}")
|
||||
.build("New York", "foo+bar")
|
||||
----
|
||||
|
||||
You can shorten it further still with a full URI template, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
|
||||
.build("New York", "foo+bar");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
|
||||
.build("New York", "foo+bar")
|
||||
----
|
||||
|
||||
The `WebClient` and the `RestTemplate` expand and encode URI templates internally through
|
||||
the `UriBuilderFactory` strategy. Both can be configured with a custom strategy,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
String baseUrl = "https://example.com";
|
||||
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
|
||||
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
|
||||
|
||||
// Customize the RestTemplate..
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(factory);
|
||||
|
||||
// Customize the WebClient..
|
||||
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val baseUrl = "https://example.com"
|
||||
val factory = DefaultUriBuilderFactory(baseUrl).apply {
|
||||
encodingMode = EncodingMode.TEMPLATE_AND_VALUES
|
||||
}
|
||||
|
||||
// Customize the RestTemplate..
|
||||
val restTemplate = RestTemplate().apply {
|
||||
uriTemplateHandler = factory
|
||||
}
|
||||
|
||||
// Customize the WebClient..
|
||||
val client = WebClient.builder().uriBuilderFactory(factory).build()
|
||||
----
|
||||
|
||||
The `DefaultUriBuilderFactory` implementation uses `UriComponentsBuilder` internally to
|
||||
expand and encode URI templates. As a factory, it provides a single place to configure
|
||||
the approach to encoding, based on one of the below encoding modes:
|
||||
|
||||
* `TEMPLATE_AND_VALUES`: Uses `UriComponentsBuilder#encode()`, corresponding to
|
||||
the first option in the earlier list, to pre-encode the URI template and strictly encode URI variables when
|
||||
expanded.
|
||||
* `VALUES_ONLY`: Does not encode the URI template and, instead, applies strict encoding
|
||||
to URI variables through `UriUtils#encodeUriVariables` prior to expanding them into the
|
||||
template.
|
||||
* `URI_COMPONENT`: Uses `UriComponents#encode()`, corresponding to the second option in the earlier list, to
|
||||
encode URI component value _after_ URI variables are expanded.
|
||||
* `NONE`: No encoding is applied.
|
||||
|
||||
The `RestTemplate` is set to `EncodingMode.URI_COMPONENT` for historic
|
||||
reasons and for backwards compatibility. The `WebClient` relies on the default value
|
||||
in `DefaultUriBuilderFactory`, which was changed from `EncodingMode.URI_COMPONENT` in
|
||||
5.0.x to `EncodingMode.TEMPLATE_AND_VALUES` in 5.1.
|
||||
@@ -1,361 +0,0 @@
|
||||
[[webflux-cors]]
|
||||
= CORS
|
||||
[.small]#<<web.adoc#mvc-cors, Web MVC>>#
|
||||
|
||||
Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
describes how to do so.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-intro]]
|
||||
== Introduction
|
||||
[.small]#<<web.adoc#mvc-cors-intro, Web MVC>>#
|
||||
|
||||
For security reasons, browsers prohibit AJAX calls to resources outside the current origin.
|
||||
For example, you could have your bank account in one tab and evil.com in another. Scripts
|
||||
from evil.com should not be able to make AJAX requests to your bank API with your
|
||||
credentials -- for example, withdrawing money from your account!
|
||||
|
||||
Cross-Origin Resource Sharing (CORS) is a https://www.w3.org/TR/cors/[W3C specification]
|
||||
implemented by https://caniuse.com/#feat=cors[most browsers] that lets you specify
|
||||
what kind of cross-domain requests are authorized, rather than using less secure and less
|
||||
powerful workarounds based on IFRAME or JSONP.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-processing]]
|
||||
== Processing
|
||||
[.small]#<<web.adoc#mvc-cors-processing, Web MVC>>#
|
||||
|
||||
The CORS specification distinguishes between preflight, simple, and actual requests.
|
||||
To learn how CORS works, you can read
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[this article], among
|
||||
many others, or see the specification for more details.
|
||||
|
||||
Spring WebFlux `HandlerMapping` implementations provide built-in support for CORS. After successfully
|
||||
mapping a request to a handler, a `HandlerMapping` checks the CORS configuration for the
|
||||
given request and handler and takes further actions. Preflight requests are handled
|
||||
directly, while simple and actual CORS requests are intercepted, validated, and have the
|
||||
required CORS response headers set.
|
||||
|
||||
In order to enable cross-origin requests (that is, the `Origin` header is present and
|
||||
differs from the host of the request), you need to have some explicitly declared CORS
|
||||
configuration. If no matching CORS configuration is found, preflight requests are
|
||||
rejected. No CORS headers are added to the responses of simple and actual CORS requests
|
||||
and, consequently, browsers reject them.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{api-spring-framework}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
|
||||
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
|
||||
use the WebFlux Java configuration to declare such mappings, which results in a single,
|
||||
global map passed to all `HandlerMapping` implementations.
|
||||
|
||||
You can combine global CORS configuration at the `HandlerMapping` level with more
|
||||
fine-grained, handler-level CORS configuration. For example, annotated controllers can use
|
||||
class- or method-level `@CrossOrigin` annotations (other handlers can implement
|
||||
`CorsConfigurationSource`).
|
||||
|
||||
The rules for combining global and local configuration are generally additive -- for example,
|
||||
all global and all local origins. For those attributes where only a single value can be
|
||||
accepted, such as `allowCredentials` and `maxAge`, the local overrides the global value. See
|
||||
{api-spring-framework}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
for more details.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
To learn more from the source or to make advanced customizations, see:
|
||||
|
||||
* `CorsConfiguration`
|
||||
* `CorsProcessor` and `DefaultCorsProcessor`
|
||||
* `AbstractHandlerMapping`
|
||||
====
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-controller]]
|
||||
== `@CrossOrigin`
|
||||
[.small]#<<web.adoc#mvc-cors-controller, Web MVC>>#
|
||||
|
||||
The {api-spring-framework}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`]
|
||||
annotation enables cross-origin requests on annotated controller methods, as the
|
||||
following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Account> retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<Void> remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
class AccountController {
|
||||
|
||||
@CrossOrigin
|
||||
@GetMapping("/{id}")
|
||||
suspend fun retrieve(@PathVariable id: Long): Account {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
suspend fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
By default, `@CrossOrigin` allows:
|
||||
|
||||
* All origins.
|
||||
* All headers.
|
||||
* All HTTP methods to which the controller method is mapped.
|
||||
|
||||
`allowCredentials` is not enabled by default, since that establishes a trust level
|
||||
that exposes sensitive user-specific information (such as cookies and CSRF tokens) and
|
||||
should be used only where appropriate. When it is enabled either `allowOrigins` must be
|
||||
set to one or more specific domain (but not the special value `"*"`) or alternatively
|
||||
the `allowOriginPatterns` property may be used to match to a dynamic set of origins.
|
||||
|
||||
`maxAge` is set to 30 minutes.
|
||||
|
||||
`@CrossOrigin` is supported at the class level, too, and inherited by all methods.
|
||||
The following example specifies a certain domain and sets `maxAge` to an hour:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Account> retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<Void> remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@CrossOrigin("https://domain2.com", maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
class AccountController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
suspend fun retrieve(@PathVariable id: Long): Account {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
suspend fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
You can use `@CrossOrigin` at both the class and the method level,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@CrossOrigin(maxAge = 3600) // <1>
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin("https://domain2.com") // <2>
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Account> retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<Void> remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Using `@CrossOrigin` at the class level.
|
||||
<2> Using `@CrossOrigin` at the method level.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@CrossOrigin(maxAge = 3600) // <1>
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
class AccountController {
|
||||
|
||||
@CrossOrigin("https://domain2.com") // <2>
|
||||
@GetMapping("/{id}")
|
||||
suspend fun retrieve(@PathVariable id: Long): Account {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
suspend fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Using `@CrossOrigin` at the class level.
|
||||
<2> Using `@CrossOrigin` at the method level.
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-global]]
|
||||
== Global Configuration
|
||||
[.small]#<<web.adoc#mvc-cors-global, Web MVC>>#
|
||||
|
||||
In addition to fine-grained, controller method-level configuration, you probably want to
|
||||
define some global CORS configuration, too. You can set URL-based `CorsConfiguration`
|
||||
mappings individually on any `HandlerMapping`. Most applications, however, use the
|
||||
WebFlux Java configuration to do that.
|
||||
|
||||
By default global configuration enables the following:
|
||||
|
||||
* All origins.
|
||||
* All headers.
|
||||
* `GET`, `HEAD`, and `POST` methods.
|
||||
|
||||
`allowedCredentials` is not enabled by default, since that establishes a trust level
|
||||
that exposes sensitive user-specific information( such as cookies and CSRF tokens) and
|
||||
should be used only where appropriate. When it is enabled either `allowOrigins` must be
|
||||
set to one or more specific domain (but not the special value `"*"`) or alternatively
|
||||
the `allowOriginPatterns` property may be used to match to a dynamic set of origins.
|
||||
|
||||
`maxAge` is set to 30 minutes.
|
||||
|
||||
To enable CORS in the WebFlux Java configuration, you can use the `CorsRegistry` callback,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("https://domain2.com")
|
||||
.allowedMethods("PUT", "DELETE")
|
||||
.allowedHeaders("header1", "header2", "header3")
|
||||
.exposedHeaders("header1", "header2")
|
||||
.allowCredentials(true).maxAge(3600);
|
||||
|
||||
// Add more mappings...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("https://domain2.com")
|
||||
.allowedMethods("PUT", "DELETE")
|
||||
.allowedHeaders("header1", "header2", "header3")
|
||||
.exposedHeaders("header1", "header2")
|
||||
.allowCredentials(true).maxAge(3600)
|
||||
|
||||
// Add more mappings...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-cors-webfilter]]
|
||||
== CORS `WebFilter`
|
||||
[.small]#<<web.adoc#mvc-cors-filter, Web MVC>>#
|
||||
|
||||
You can apply CORS support through the built-in
|
||||
{api-spring-framework}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a
|
||||
good fit with <<webflux-fn, functional endpoints>>.
|
||||
|
||||
NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring
|
||||
Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for
|
||||
CORS.
|
||||
|
||||
To configure the filter, you can declare a `CorsWebFilter` bean and pass a
|
||||
`CorsConfigurationSource` to its constructor, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Bean
|
||||
CorsWebFilter corsFilter() {
|
||||
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
|
||||
// Possibly...
|
||||
// config.applyPermitDefaultValues()
|
||||
|
||||
config.setAllowCredentials(true);
|
||||
config.addAllowedOrigin("https://domain1.com");
|
||||
config.addAllowedHeader("*");
|
||||
config.addAllowedMethod("*");
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
return new CorsWebFilter(source);
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Bean
|
||||
fun corsFilter(): CorsWebFilter {
|
||||
|
||||
val config = CorsConfiguration()
|
||||
|
||||
// Possibly...
|
||||
// config.applyPermitDefaultValues()
|
||||
|
||||
config.allowCredentials = true
|
||||
config.addAllowedOrigin("https://domain1.com")
|
||||
config.addAllowedHeader("*")
|
||||
config.addAllowedMethod("*")
|
||||
|
||||
val source = UrlBasedCorsConfigurationSource().apply {
|
||||
registerCorsConfiguration("/**", config)
|
||||
}
|
||||
return CorsWebFilter(source)
|
||||
}
|
||||
----
|
||||
@@ -1,901 +0,0 @@
|
||||
[[webflux-fn]]
|
||||
= Functional Endpoints
|
||||
[.small]#<<web.adoc#webmvc-fn, Web MVC>>#
|
||||
|
||||
Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions
|
||||
are used to route and handle requests and contracts are designed for immutability.
|
||||
It is an alternative to the annotation-based programming model but otherwise runs on
|
||||
the same <<web-reactive.adoc#webflux-reactive-spring-web>> foundation.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-overview]]
|
||||
== Overview
|
||||
[.small]#<<web.adoc#webmvc-fn-overview, Web MVC>>#
|
||||
|
||||
In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes
|
||||
`ServerRequest` and returns a delayed `ServerResponse` (i.e. `Mono<ServerResponse>`).
|
||||
Both the request and the response object have immutable contracts that offer JDK 8-friendly
|
||||
access to the HTTP request and response.
|
||||
`HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the
|
||||
annotation-based programming model.
|
||||
|
||||
Incoming requests are routed to a handler function with a `RouterFunction`: a function that
|
||||
takes `ServerRequest` and returns a delayed `HandlerFunction` (i.e. `Mono<HandlerFunction>`).
|
||||
When the router function matches, a handler function is returned; otherwise an empty Mono.
|
||||
`RouterFunction` is the equivalent of a `@RequestMapping` annotation, but with the major
|
||||
difference that router functions provide not just data, but also behavior.
|
||||
|
||||
`RouterFunctions.route()` provides a router builder that facilitates the creation of routers,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
|
||||
PersonRepository repository = ...
|
||||
PersonHandler handler = new PersonHandler(repository);
|
||||
|
||||
RouterFunction<ServerResponse> route = route() <1>
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST("/person", handler::createPerson)
|
||||
.build();
|
||||
|
||||
|
||||
public class PersonHandler {
|
||||
|
||||
// ...
|
||||
|
||||
public Mono<ServerResponse> listPeople(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> createPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using `route()`.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val repository: PersonRepository = ...
|
||||
val handler = PersonHandler(repository)
|
||||
|
||||
val route = coRouter { // <1>
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/person/{id}", handler::getPerson)
|
||||
GET("/person", handler::listPeople)
|
||||
}
|
||||
POST("/person", handler::createPerson)
|
||||
}
|
||||
|
||||
|
||||
class PersonHandler(private val repository: PersonRepository) {
|
||||
|
||||
// ...
|
||||
|
||||
suspend fun listPeople(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
|
||||
suspend fun createPerson(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
|
||||
suspend fun getPerson(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using Coroutines router DSL; a Reactive alternative is also available via `router { }`.
|
||||
|
||||
One way to run a `RouterFunction` is to turn it into an `HttpHandler` and install it
|
||||
through one of the built-in <<web-reactive.adoc#webflux-httphandler, server adapters>>:
|
||||
|
||||
* `RouterFunctions.toHttpHandler(RouterFunction)`
|
||||
* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)`
|
||||
|
||||
Most applications can run through the WebFlux Java configuration, see <<webflux-fn-running>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-handler-functions]]
|
||||
== HandlerFunction
|
||||
[.small]#<<web.adoc#webmvc-fn-handler-functions, Web MVC>>#
|
||||
|
||||
`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly
|
||||
access to the HTTP request and response.
|
||||
Both request and response provide https://www.reactive-streams.org[Reactive Streams] back pressure
|
||||
against the body streams.
|
||||
The request body is represented with a Reactor `Flux` or `Mono`.
|
||||
The response body is represented with any Reactive Streams `Publisher`, including `Flux` and `Mono`.
|
||||
For more on that, see <<web-reactive.adoc#webflux-reactive-libraries, Reactive Libraries>>.
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-request]]
|
||||
=== ServerRequest
|
||||
|
||||
`ServerRequest` provides access to the HTTP method, URI, headers, and query parameters,
|
||||
while access to the body is provided through the `body` methods.
|
||||
|
||||
The following example extracts the request body to a `Mono<String>`:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<String> string = request.bodyToMono(String.class);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val string = request.awaitBody<String>()
|
||||
----
|
||||
|
||||
|
||||
The following example extracts the body to a `Flux<Person>` (or a `Flow<Person>` in Kotlin),
|
||||
where `Person` objects are decoded from some serialized form, such as JSON or XML:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Flux<Person> people = request.bodyToFlux(Person.class);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val people = request.bodyToFlow<Person>()
|
||||
----
|
||||
|
||||
The preceding examples are shortcuts that use the more general `ServerRequest.body(BodyExtractor)`,
|
||||
which accepts the `BodyExtractor` functional strategy interface. The utility class
|
||||
`BodyExtractors` provides access to a number of instances. For example, the preceding examples can
|
||||
also be written as follows:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
|
||||
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
|
||||
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
|
||||
----
|
||||
|
||||
The following example shows how to access form data:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<MultiValueMap<String, String>> map = request.formData();
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val map = request.awaitFormData()
|
||||
----
|
||||
|
||||
The following example shows how to access multipart data as a map:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<MultiValueMap<String, Part>> map = request.multipartData();
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val map = request.awaitMultipartData()
|
||||
----
|
||||
|
||||
The following example shows how to access multipart data, one at a time, in streaming fashion:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
Flux<PartEvent> allPartEvents = request.bodyToFlux(PartEvent.class);
|
||||
allPartsEvents.windowUntil(PartEvent::isLast)
|
||||
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
|
||||
if (signal.hasValue()) {
|
||||
PartEvent event = signal.get();
|
||||
if (event instanceof FormPartEvent formEvent) {
|
||||
String value = formEvent.value();
|
||||
// handle form field
|
||||
}
|
||||
else if (event instanceof FilePartEvent fileEvent) {
|
||||
String filename = fileEvent.filename();
|
||||
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
|
||||
// handle file upload
|
||||
}
|
||||
else {
|
||||
return Mono.error(new RuntimeException("Unexpected event: " + event));
|
||||
}
|
||||
}
|
||||
else {
|
||||
return partEvents; // either complete or error signal
|
||||
}
|
||||
}));
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val parts = request.bodyToFlux<PartEvent>()
|
||||
allPartsEvents.windowUntil(PartEvent::isLast)
|
||||
.concatMap {
|
||||
it.switchOnFirst { signal, partEvents ->
|
||||
if (signal.hasValue()) {
|
||||
val event = signal.get()
|
||||
if (event is FormPartEvent) {
|
||||
val value: String = event.value();
|
||||
// handle form field
|
||||
} else if (event is FilePartEvent) {
|
||||
val filename: String = event.filename();
|
||||
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content);
|
||||
// handle file upload
|
||||
} else {
|
||||
return Mono.error(RuntimeException("Unexpected event: " + event));
|
||||
}
|
||||
} else {
|
||||
return partEvents; // either complete or error signal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Note that the body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
|
||||
|
||||
[[webflux-fn-response]]
|
||||
=== ServerResponse
|
||||
|
||||
`ServerResponse` provides access to the HTTP response and, since it is immutable, you can use
|
||||
a `build` method to create it. You can use the builder to set the response status, to add response
|
||||
headers, or to provide a body. The following example creates a 200 (OK) response with JSON
|
||||
content:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<Person> person = ...
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val person: Person = ...
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
|
||||
----
|
||||
|
||||
The following example shows how to build a 201 (CREATED) response with a `Location` header and no body:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI location = ...
|
||||
ServerResponse.created(location).build();
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val location: URI = ...
|
||||
ServerResponse.created(location).build()
|
||||
----
|
||||
|
||||
Depending on the codec used, it is possible to pass hint parameters to customize how the
|
||||
body is serialized or deserialized. For example, to specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON view]:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
|
||||
----
|
||||
|
||||
|
||||
[[webflux-fn-handler-classes]]
|
||||
=== Handler Classes
|
||||
|
||||
We can write a handler function as a lambda, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
HandlerFunction<ServerResponse> helloWorld =
|
||||
request -> ServerResponse.ok().bodyValue("Hello World");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
|
||||
----
|
||||
|
||||
That is convenient, but in an application we need multiple functions, and multiple inline
|
||||
lambda's can get messy.
|
||||
Therefore, it is useful to group related handler functions together into a handler class, which
|
||||
has a similar role as `@Controller` in an annotation-based application.
|
||||
For example, the following class exposes a reactive `Person` repository:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
|
||||
|
||||
public class PersonHandler {
|
||||
|
||||
private final PersonRepository repository;
|
||||
|
||||
public PersonHandler(PersonRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> listPeople(ServerRequest request) { // <1>
|
||||
Flux<Person> people = repository.allPeople();
|
||||
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> createPerson(ServerRequest request) { // <2>
|
||||
Mono<Person> person = request.bodyToMono(Person.class);
|
||||
return ok().build(repository.savePerson(person));
|
||||
}
|
||||
|
||||
public Mono<ServerResponse> getPerson(ServerRequest request) { // <3>
|
||||
int personId = Integer.valueOf(request.pathVariable("id"));
|
||||
return repository.getPerson(personId)
|
||||
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as
|
||||
JSON.
|
||||
<2> `createPerson` is a handler function that stores a new `Person` contained in the request body.
|
||||
Note that `PersonRepository.savePerson(Person)` returns `Mono<Void>`: an empty `Mono` that emits
|
||||
a completion signal when the person has been read from the request and stored. So we use the
|
||||
`build(Publisher<Void>)` method to send a response when that completion signal is received (that is,
|
||||
when the `Person` has been saved).
|
||||
<3> `getPerson` is a handler function that returns a single person, identified by the `id` path
|
||||
variable. We retrieve that `Person` from the repository and create a JSON response, if it is
|
||||
found. If it is not found, we use `switchIfEmpty(Mono<T>)` to return a 404 Not Found response.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class PersonHandler(private val repository: PersonRepository) {
|
||||
|
||||
suspend fun listPeople(request: ServerRequest): ServerResponse { // <1>
|
||||
val people: Flow<Person> = repository.allPeople()
|
||||
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
|
||||
}
|
||||
|
||||
suspend fun createPerson(request: ServerRequest): ServerResponse { // <2>
|
||||
val person = request.awaitBody<Person>()
|
||||
repository.savePerson(person)
|
||||
return ok().buildAndAwait()
|
||||
}
|
||||
|
||||
suspend fun getPerson(request: ServerRequest): ServerResponse { // <3>
|
||||
val personId = request.pathVariable("id").toInt()
|
||||
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
|
||||
?: ServerResponse.notFound().buildAndAwait()
|
||||
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as
|
||||
JSON.
|
||||
<2> `createPerson` is a handler function that stores a new `Person` contained in the request body.
|
||||
Note that `PersonRepository.savePerson(Person)` is a suspending function with no return type.
|
||||
<3> `getPerson` is a handler function that returns a single person, identified by the `id` path
|
||||
variable. We retrieve that `Person` from the repository and create a JSON response, if it is
|
||||
found. If it is not found, we return a 404 Not Found response.
|
||||
|
||||
|
||||
[[webflux-fn-handler-validation]]
|
||||
=== Validation
|
||||
|
||||
A functional endpoint can use Spring's <<core.adoc#validation, validation facilities>> to
|
||||
apply validation to the request body. For example, given a custom Spring
|
||||
<<core.adoc#validation, Validator>> implementation for a `Person`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
public class PersonHandler {
|
||||
|
||||
private final Validator validator = new PersonValidator(); // <1>
|
||||
|
||||
// ...
|
||||
|
||||
public Mono<ServerResponse> createPerson(ServerRequest request) {
|
||||
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); // <2>
|
||||
return ok().build(repository.savePerson(person));
|
||||
}
|
||||
|
||||
private void validate(Person person) {
|
||||
Errors errors = new BeanPropertyBindingResult(person, "person");
|
||||
validator.validate(person, errors);
|
||||
if (errors.hasErrors()) {
|
||||
throw new ServerWebInputException(errors.toString()); // <3>
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create `Validator` instance.
|
||||
<2> Apply validation.
|
||||
<3> Raise exception for a 400 response.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class PersonHandler(private val repository: PersonRepository) {
|
||||
|
||||
private val validator = PersonValidator() // <1>
|
||||
|
||||
// ...
|
||||
|
||||
suspend fun createPerson(request: ServerRequest): ServerResponse {
|
||||
val person = request.awaitBody<Person>()
|
||||
validate(person) // <2>
|
||||
repository.savePerson(person)
|
||||
return ok().buildAndAwait()
|
||||
}
|
||||
|
||||
private fun validate(person: Person) {
|
||||
val errors: Errors = BeanPropertyBindingResult(person, "person");
|
||||
validator.validate(person, errors);
|
||||
if (errors.hasErrors()) {
|
||||
throw ServerWebInputException(errors.toString()) // <3>
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create `Validator` instance.
|
||||
<2> Apply validation.
|
||||
<3> Raise exception for a 400 response.
|
||||
|
||||
Handlers can also use the standard bean validation API (JSR-303) by creating and injecting
|
||||
a global `Validator` instance based on `LocalValidatorFactoryBean`.
|
||||
See <<core.adoc#validation-beanvalidation, Spring Validation>>.
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-router-functions]]
|
||||
== `RouterFunction`
|
||||
[.small]#<<web.adoc#webmvc-fn-router-functions, Web MVC>>#
|
||||
|
||||
Router functions are used to route the requests to the corresponding `HandlerFunction`.
|
||||
Typically, you do not write router functions yourself, but rather use a method on the
|
||||
`RouterFunctions` utility class to create one.
|
||||
`RouterFunctions.route()` (no parameters) provides you with a fluent builder for creating a router
|
||||
function, whereas `RouterFunctions.route(RequestPredicate, HandlerFunction)` offers a direct way
|
||||
to create a router.
|
||||
|
||||
Generally, it is recommended to use the `route()` builder, as it provides
|
||||
convenient short-cuts for typical mapping scenarios without requiring hard-to-discover
|
||||
static imports.
|
||||
For instance, the router function builder offers the method `GET(String, HandlerFunction)` to create a mapping for GET requests; and `POST(String, HandlerFunction)` for POSTs.
|
||||
|
||||
Besides HTTP method-based mapping, the route builder offers a way to introduce additional
|
||||
predicates when mapping to requests.
|
||||
For each HTTP method there is an overloaded variant that takes a `RequestPredicate` as a
|
||||
parameter, though which additional constraints can be expressed.
|
||||
|
||||
|
||||
[[webflux-fn-predicates]]
|
||||
=== Predicates
|
||||
|
||||
You can write your own `RequestPredicate`, but the `RequestPredicates` utility class
|
||||
offers commonly used implementations, based on the request path, HTTP method, content-type,
|
||||
and so on.
|
||||
The following example uses a request predicate to create a constraint based on the `Accept`
|
||||
header:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = RouterFunctions.route()
|
||||
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
|
||||
request -> ServerResponse.ok().bodyValue("Hello World")).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val route = coRouter {
|
||||
GET("/hello-world", accept(TEXT_PLAIN)) {
|
||||
ServerResponse.ok().bodyValueAndAwait("Hello World")
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
You can compose multiple request predicates together by using:
|
||||
|
||||
* `RequestPredicate.and(RequestPredicate)` -- both must match.
|
||||
* `RequestPredicate.or(RequestPredicate)` -- either can match.
|
||||
|
||||
Many of the predicates from `RequestPredicates` are composed.
|
||||
For example, `RequestPredicates.GET(String)` is composed from `RequestPredicates.method(HttpMethod)`
|
||||
and `RequestPredicates.path(String)`.
|
||||
The example shown above also uses two request predicates, as the builder uses
|
||||
`RequestPredicates.GET` internally, and composes that with the `accept` predicate.
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-routes]]
|
||||
=== Routes
|
||||
|
||||
Router functions are evaluated in order: if the first route does not match, the
|
||||
second is evaluated, and so on.
|
||||
Therefore, it makes sense to declare more specific routes before general ones.
|
||||
This is also important when registering router functions as Spring beans, as will
|
||||
be described later.
|
||||
Note that this behavior is different from the annotation-based programming model, where the
|
||||
"most specific" controller method is picked automatically.
|
||||
|
||||
When using the router function builder, all defined routes are composed into one
|
||||
`RouterFunction` that is returned from `build()`.
|
||||
There are also other ways to compose multiple router functions together:
|
||||
|
||||
* `add(RouterFunction)` on the `RouterFunctions.route()` builder
|
||||
* `RouterFunction.and(RouterFunction)`
|
||||
* `RouterFunction.andRoute(RequestPredicate, HandlerFunction)` -- shortcut for
|
||||
`RouterFunction.and()` with nested `RouterFunctions.route()`.
|
||||
|
||||
The following example shows the composition of four routes:
|
||||
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
|
||||
|
||||
PersonRepository repository = ...
|
||||
PersonHandler handler = new PersonHandler(repository);
|
||||
|
||||
RouterFunction<ServerResponse> otherRoute = ...
|
||||
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
|
||||
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
|
||||
.POST("/person", handler::createPerson) // <3>
|
||||
.add(otherRoute) // <4>
|
||||
.build();
|
||||
----
|
||||
<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to
|
||||
`PersonHandler.getPerson`
|
||||
<2> `GET /person` with an `Accept` header that matches JSON is routed to
|
||||
`PersonHandler.listPeople`
|
||||
<3> `POST /person` with no additional predicates is mapped to
|
||||
`PersonHandler.createPerson`, and
|
||||
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.http.MediaType.APPLICATION_JSON
|
||||
|
||||
val repository: PersonRepository = ...
|
||||
val handler = PersonHandler(repository);
|
||||
|
||||
val otherRoute: RouterFunction<ServerResponse> = coRouter { }
|
||||
|
||||
val route = coRouter {
|
||||
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
|
||||
GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
|
||||
POST("/person", handler::createPerson) // <3>
|
||||
}.and(otherRoute) // <4>
|
||||
----
|
||||
<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to
|
||||
`PersonHandler.getPerson`
|
||||
<2> `GET /person` with an `Accept` header that matches JSON is routed to
|
||||
`PersonHandler.listPeople`
|
||||
<3> `POST /person` with no additional predicates is mapped to
|
||||
`PersonHandler.createPerson`, and
|
||||
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built.
|
||||
|
||||
|
||||
=== Nested Routes
|
||||
|
||||
It is common for a group of router functions to have a shared predicate, for instance a
|
||||
shared path. In the example above, the shared predicate would be a path predicate that
|
||||
matches `/person`, used by three of the routes. When using annotations, you would remove
|
||||
this duplication by using a type-level `@RequestMapping` annotation that maps to
|
||||
`/person`. In WebFlux.fn, path predicates can be shared through the `path` method on the
|
||||
router function builder. For instance, the last few lines of the example above can be
|
||||
improved in the following way by using nested routes:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", builder -> builder // <1>
|
||||
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST(handler::createPerson))
|
||||
.build();
|
||||
----
|
||||
<1> Note that second parameter of `path` is a consumer that takes the router builder.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val route = coRouter { // <1>
|
||||
"/person".nest {
|
||||
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using Coroutines router DSL; a Reactive alternative is also available via `router { }`.
|
||||
|
||||
Though path-based nesting is the most common, you can nest on any kind of predicate by using
|
||||
the `nest` method on the builder.
|
||||
The above still contains some duplication in the form of the shared `Accept`-header predicate.
|
||||
We can further improve by using the `nest` method together with `accept`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(handler::createPerson))
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val route = coRouter {
|
||||
"/person".nest {
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET(handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[[webflux-fn-running]]
|
||||
== Running a Server
|
||||
[.small]#<<web.adoc#webmvc-fn-running, Web MVC>>#
|
||||
|
||||
How do you run a router function in an HTTP server? A simple option is to convert a router
|
||||
function to an `HttpHandler` by using one of the following:
|
||||
|
||||
* `RouterFunctions.toHttpHandler(RouterFunction)`
|
||||
* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)`
|
||||
|
||||
You can then use the returned `HttpHandler` with a number of server adapters by following
|
||||
<<web-reactive.adoc#webflux-httphandler, HttpHandler>> for server-specific instructions.
|
||||
|
||||
A more typical option, also used by Spring Boot, is to run with a
|
||||
<<web-reactive.adoc#webflux-dispatcher-handler, `DispatcherHandler`>>-based setup through the
|
||||
<<web-reactive.adoc#webflux-config>>, which uses Spring configuration to declare the
|
||||
components required to process requests. The WebFlux Java configuration declares the following
|
||||
infrastructure components to support functional endpoints:
|
||||
|
||||
* `RouterFunctionMapping`: Detects one or more `RouterFunction<?>` beans in the Spring
|
||||
configuration, <<core.adoc#beans-factory-ordered, orders them>>, combines them through
|
||||
`RouterFunction.andOther`, and routes requests to the resulting composed `RouterFunction`.
|
||||
* `HandlerFunctionAdapter`: Simple adapter that lets `DispatcherHandler` invoke
|
||||
a `HandlerFunction` that was mapped to a request.
|
||||
* `ServerResponseResultHandler`: Handles the result from the invocation of a
|
||||
`HandlerFunction` by invoking the `writeTo` method of the `ServerResponse`.
|
||||
|
||||
The preceding components let functional endpoints fit within the `DispatcherHandler` request
|
||||
processing lifecycle and also (potentially) run side by side with annotated controllers, if
|
||||
any are declared. It is also how functional endpoints are enabled by the Spring Boot WebFlux
|
||||
starter.
|
||||
|
||||
The following example shows a WebFlux Java configuration (see
|
||||
<<web-reactive.adoc#webflux-dispatcher-handler, DispatcherHandler>> for how to run it):
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Bean
|
||||
public RouterFunction<?> routerFunctionA() {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<?> routerFunctionB() {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
@Override
|
||||
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
|
||||
// configure message conversion...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
// configure CORS...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
// configure view resolution for HTML rendering...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
@Bean
|
||||
fun routerFunctionA(): RouterFunction<*> {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun routerFunctionB(): RouterFunction<*> {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
|
||||
// configure message conversion...
|
||||
}
|
||||
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
// configure CORS...
|
||||
}
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
// configure view resolution for HTML rendering...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-fn-handler-filter-function]]
|
||||
== Filtering Handler Functions
|
||||
[.small]#<<web.adoc#webmvc-fn-handler-filter-function, Web MVC>>#
|
||||
|
||||
You can filter handler functions by using the `before`, `after`, or `filter` methods on the routing
|
||||
function builder.
|
||||
With annotations, you can achieve similar functionality by using `@ControllerAdvice`, a `ServletFilter`, or both.
|
||||
The filter will apply to all routes that are built by the builder.
|
||||
This means that filters defined in nested routes do not apply to "top-level" routes.
|
||||
For instance, consider the following example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople)
|
||||
.before(request -> ServerRequest.from(request) // <1>
|
||||
.header("X-RequestHeader", "Value")
|
||||
.build()))
|
||||
.POST(handler::createPerson))
|
||||
.after((request, response) -> logResponse(response)) // <2>
|
||||
.build();
|
||||
----
|
||||
<1> The `before` filter that adds a custom request header is only applied to the two GET routes.
|
||||
<2> The `after` filter that logs the response is applied to all routes, including the nested ones.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val route = router {
|
||||
"/person".nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
before { // <1>
|
||||
ServerRequest.from(it)
|
||||
.header("X-RequestHeader", "Value").build()
|
||||
}
|
||||
POST(handler::createPerson)
|
||||
after { _, response -> // <2>
|
||||
logResponse(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> The `before` filter that adds a custom request header is only applied to the two GET routes.
|
||||
<2> The `after` filter that logs the response is applied to all routes, including the nested ones.
|
||||
|
||||
|
||||
The `filter` method on the router builder takes a `HandlerFilterFunction`: a
|
||||
function that takes a `ServerRequest` and `HandlerFunction` and returns a `ServerResponse`.
|
||||
The handler function parameter represents the next element in the chain.
|
||||
This is typically the handler that is routed to, but it can also be another
|
||||
filter if multiple are applied.
|
||||
|
||||
Now we can add a simple security filter to our route, assuming that we have a `SecurityManager` that
|
||||
can determine whether a particular path is allowed.
|
||||
The following example shows how to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
SecurityManager securityManager = ...
|
||||
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(handler::createPerson))
|
||||
.filter((request, next) -> {
|
||||
if (securityManager.allowAccessTo(request.path())) {
|
||||
return next.handle(request);
|
||||
}
|
||||
else {
|
||||
return ServerResponse.status(UNAUTHORIZED).build();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val securityManager: SecurityManager = ...
|
||||
|
||||
val route = router {
|
||||
("/person" and accept(APPLICATION_JSON)).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
filter { request, next ->
|
||||
if (securityManager.allowAccessTo(request.path())) {
|
||||
next(request)
|
||||
}
|
||||
else {
|
||||
status(UNAUTHORIZED).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional.
|
||||
We only let the handler function be run when access is allowed.
|
||||
|
||||
Besides using the `filter` method on the router function builder, it is possible to apply a
|
||||
filter to an existing router function via `RouterFunction.filter(HandlerFilterFunction)`.
|
||||
|
||||
NOTE: CORS support for functional endpoints is provided through a dedicated
|
||||
<<webflux-cors.adoc#webflux-cors-webfilter, `CorsWebFilter`>>.
|
||||
@@ -1,408 +0,0 @@
|
||||
[[webflux-view]]
|
||||
= View Technologies
|
||||
[.small]#<<web.adoc#mvc-view, Web MVC>>#
|
||||
|
||||
The use of view technologies in Spring WebFlux is pluggable. Whether you decide to
|
||||
use Thymeleaf, FreeMarker, or some other view technology is primarily a matter of a
|
||||
configuration change. This chapter covers the view technologies integrated with Spring
|
||||
WebFlux. We assume you are already familiar with <<webflux-viewresolution>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-view-thymeleaf]]
|
||||
== Thymeleaf
|
||||
[.small]#<<web.adoc#mvc-view-thymeleaf, Web MVC>>#
|
||||
|
||||
Thymeleaf is a modern server-side Java template engine that emphasizes natural HTML
|
||||
templates that can be previewed in a browser by double-clicking, which is very
|
||||
helpful for independent work on UI templates (for example, by a designer) without the need for a
|
||||
running server. Thymeleaf offers an extensive set of features, and it is actively developed
|
||||
and maintained. For a more complete introduction, see the
|
||||
https://www.thymeleaf.org/[Thymeleaf] project home page.
|
||||
|
||||
The Thymeleaf integration with Spring WebFlux is managed by the Thymeleaf project. The
|
||||
configuration involves a few bean declarations, such as
|
||||
`SpringResourceTemplateResolver`, `SpringWebFluxTemplateEngine`, and
|
||||
`ThymeleafReactiveViewResolver`. For more details, see
|
||||
https://www.thymeleaf.org/documentation.html[Thymeleaf+Spring] and the WebFlux integration
|
||||
https://web.archive.org/web/20210623051330/http%3A//forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html[announcement].
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-view-freemarker]]
|
||||
== FreeMarker
|
||||
[.small]#<<web.adoc#mvc-view-freemarker, Web MVC>>#
|
||||
|
||||
https://freemarker.apache.org/[Apache FreeMarker] is a template engine for generating any
|
||||
kind of text output from HTML to email and others. The Spring Framework has built-in
|
||||
integration for using Spring WebFlux with FreeMarker templates.
|
||||
|
||||
|
||||
|
||||
[[webflux-view-freemarker-contextconfig]]
|
||||
=== View Configuration
|
||||
[.small]#<<web.adoc#mvc-view-freemarker-contextconfig, Web MVC>>#
|
||||
|
||||
The following example shows how to configure FreeMarker as a view technology:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.freeMarker();
|
||||
}
|
||||
|
||||
// Configure FreeMarker...
|
||||
|
||||
@Bean
|
||||
public FreeMarkerConfigurer freeMarkerConfigurer() {
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.freeMarker()
|
||||
}
|
||||
|
||||
// Configure FreeMarker...
|
||||
|
||||
@Bean
|
||||
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
|
||||
setTemplateLoaderPath("classpath:/templates/freemarker")
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Your templates need to be stored in the directory specified by the `FreeMarkerConfigurer`,
|
||||
shown in the preceding example. Given the preceding configuration, if your controller
|
||||
returns the view name, `welcome`, the resolver looks for the
|
||||
`classpath:/templates/freemarker/welcome.ftl` template.
|
||||
|
||||
|
||||
|
||||
[[webflux-views-freemarker]]
|
||||
=== FreeMarker Configuration
|
||||
[.small]#<<web.adoc#mvc-views-freemarker, Web MVC>>#
|
||||
|
||||
You can pass FreeMarker 'Settings' and 'SharedVariables' directly to the FreeMarker
|
||||
`Configuration` object (which is managed by Spring) by setting the appropriate bean
|
||||
properties on the `FreeMarkerConfigurer` bean. The `freemarkerSettings` property requires
|
||||
a `java.util.Properties` object, and the `freemarkerVariables` property requires a
|
||||
`java.util.Map`. The following example shows how to use a `FreeMarkerConfigurer`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
// ...
|
||||
|
||||
@Bean
|
||||
public FreeMarkerConfigurer freeMarkerConfigurer() {
|
||||
Map<String, Object> variables = new HashMap<>();
|
||||
variables.put("xml_escape", new XmlEscape());
|
||||
|
||||
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
|
||||
configurer.setTemplateLoaderPath("classpath:/templates");
|
||||
configurer.setFreemarkerVariables(variables);
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
// ...
|
||||
|
||||
@Bean
|
||||
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
|
||||
setTemplateLoaderPath("classpath:/templates")
|
||||
setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
See the FreeMarker documentation for details of settings and variables as they apply to
|
||||
the `Configuration` object.
|
||||
|
||||
|
||||
|
||||
[[webflux-view-freemarker-forms]]
|
||||
=== Form Handling
|
||||
[.small]#<<web.adoc#mvc-view-freemarker-forms, Web MVC>>#
|
||||
|
||||
Spring provides a tag library for use in JSPs that contains, among others, a
|
||||
`<spring:bind/>` element. This element primarily lets forms display values from
|
||||
form-backing objects and show the results of failed validations from a `Validator` in the
|
||||
web or business tier. Spring also has support for the same functionality in FreeMarker,
|
||||
with additional convenience macros for generating form input elements themselves.
|
||||
|
||||
|
||||
[[webflux-view-bind-macros]]
|
||||
==== The Bind Macros
|
||||
[.small]#<<web.adoc#mvc-view-bind-macros, Web MVC>>#
|
||||
|
||||
A standard set of macros are maintained within the `spring-webflux.jar` file for
|
||||
FreeMarker, so they are always available to a suitably configured application.
|
||||
|
||||
Some of the macros defined in the Spring templating libraries are considered internal
|
||||
(private), but no such scoping exists in the macro definitions, making all macros visible
|
||||
to calling code and user templates. The following sections concentrate only on the macros
|
||||
you need to directly call from within your templates. If you wish to view the macro code
|
||||
directly, the file is called `spring.ftl` and is in the
|
||||
`org.springframework.web.reactive.result.view.freemarker` package.
|
||||
|
||||
For additional details on binding support, see <<web.adoc#mvc-view-simple-binding, Simple
|
||||
Binding>> for Spring MVC.
|
||||
|
||||
|
||||
[[webflux-views-form-macros]]
|
||||
==== Form Macros
|
||||
|
||||
For details on Spring's form macro support for FreeMarker templates, consult the following
|
||||
sections of the Spring MVC documentation.
|
||||
|
||||
* <<web.adoc#mvc-views-form-macros, Input Macros>>
|
||||
* <<web.adoc#mvc-views-form-macros-input, Input Fields>>
|
||||
* <<web.adoc#mvc-views-form-macros-select, Selection Fields>>
|
||||
* <<web.adoc#mvc-views-form-macros-html-escaping, HTML Escaping>>
|
||||
|
||||
|
||||
|
||||
[[webflux-view-script]]
|
||||
== Script Views
|
||||
[.small]#<<web.adoc#mvc-view-script, Web MVC>>#
|
||||
|
||||
The Spring Framework has a built-in integration for using Spring WebFlux with any
|
||||
templating library that can run on top of the
|
||||
https://www.jcp.org/en/jsr/detail?id=223[JSR-223] Java scripting engine.
|
||||
The following table shows the templating libraries that we have tested on different script engines:
|
||||
|
||||
[%header]
|
||||
|===
|
||||
|Scripting Library |Scripting Engine
|
||||
|https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://facebook.github.io/react/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://www.embeddedjs.com/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://www.stuartellis.name/articles/erb/[ERB] |https://www.jruby.org[JRuby]
|
||||
|https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython]
|
||||
|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |https://kotlinlang.org/[Kotlin]
|
||||
|===
|
||||
|
||||
TIP: The basic rule for integrating any other script engine is that it must implement the
|
||||
`ScriptEngine` and `Invocable` interfaces.
|
||||
|
||||
|
||||
|
||||
[[webflux-view-script-dependencies]]
|
||||
=== Requirements
|
||||
[.small]#<<web.adoc#mvc-view-script-dependencies, Web MVC>>#
|
||||
|
||||
You need to have the script engine on your classpath, the details of which vary by script engine:
|
||||
|
||||
* The https://openjdk.java.net/projects/nashorn/[Nashorn] JavaScript engine is provided with
|
||||
Java 8+. Using the latest update release available is highly recommended.
|
||||
* https://www.jruby.org[JRuby] should be added as a dependency for Ruby support.
|
||||
* https://www.jython.org[Jython] should be added as a dependency for Python support.
|
||||
* `org.jetbrains.kotlin:kotlin-script-util` dependency and a `META-INF/services/javax.script.ScriptEngineFactory`
|
||||
file containing a `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`
|
||||
line should be added for Kotlin script support. See
|
||||
https://github.com/sdeleuze/kotlin-script-templating[this example] for more detail.
|
||||
|
||||
You need to have the script templating library. One way to do that for JavaScript is
|
||||
through https://www.webjars.org/[WebJars].
|
||||
|
||||
|
||||
|
||||
[[webflux-view-script-integrate]]
|
||||
=== Script Templates
|
||||
[.small]#<<web.adoc#mvc-view-script-integrate, Web MVC>>#
|
||||
|
||||
You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use,
|
||||
the script files to load, what function to call to render templates, and so on.
|
||||
The following example uses Mustache templates and the Nashorn JavaScript engine:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.scriptTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ScriptTemplateConfigurer configurer() {
|
||||
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
|
||||
configurer.setEngineName("nashorn");
|
||||
configurer.setScripts("mustache.js");
|
||||
configurer.setRenderObject("Mustache");
|
||||
configurer.setRenderFunction("render");
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.scriptTemplate()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun configurer() = ScriptTemplateConfigurer().apply {
|
||||
engineName = "nashorn"
|
||||
setScripts("mustache.js")
|
||||
renderObject = "Mustache"
|
||||
renderFunction = "render"
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The `render` function is called with the following parameters:
|
||||
|
||||
* `String template`: The template content
|
||||
* `Map model`: The view model
|
||||
* `RenderingContext renderingContext`: The
|
||||
{api-spring-framework}/web/servlet/view/script/RenderingContext.html[`RenderingContext`]
|
||||
that gives access to the application context, the locale, the template loader, and the
|
||||
URL (since 5.0)
|
||||
|
||||
`Mustache.render()` is natively compatible with this signature, so you can call it directly.
|
||||
|
||||
If your templating technology requires some customization, you can provide a script that
|
||||
implements a custom render function. For example, https://handlebarsjs.com[Handlerbars]
|
||||
needs to compile templates before using them and requires a
|
||||
https://en.wikipedia.org/wiki/Polyfill[polyfill] in order to emulate some
|
||||
browser facilities not available in the server-side script engine.
|
||||
The following example shows how to set a custom render function:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.scriptTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ScriptTemplateConfigurer configurer() {
|
||||
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
|
||||
configurer.setEngineName("nashorn");
|
||||
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
|
||||
configurer.setRenderFunction("render");
|
||||
configurer.setSharedEngine(false);
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.scriptTemplate()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun configurer() = ScriptTemplateConfigurer().apply {
|
||||
engineName = "nashorn"
|
||||
setScripts("polyfill.js", "handlebars.js", "render.js")
|
||||
renderFunction = "render"
|
||||
isSharedEngine = false
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe
|
||||
script engines with templating libraries not designed for concurrency, such as Handlebars or
|
||||
React running on Nashorn. In that case, Java SE 8 update 60 is required, due to
|
||||
https://bugs.openjdk.java.net/browse/JDK-8076099[this bug], but it is generally
|
||||
recommended to use a recent Java SE patch release in any case.
|
||||
|
||||
`polyfill.js` defines only the `window` object needed by Handlebars to run properly,
|
||||
as the following snippet shows:
|
||||
|
||||
[source,javascript,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
var window = {};
|
||||
----
|
||||
|
||||
This basic `render.js` implementation compiles the template before using it. A production
|
||||
ready implementation should also store and reused cached templates or pre-compiled templates.
|
||||
This can be done on the script side, as well as any customization you need (managing
|
||||
template engine configuration for example).
|
||||
The following example shows how compile a template:
|
||||
|
||||
[source,javascript,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
function render(template, model) {
|
||||
var compiledTemplate = Handlebars.compile(template);
|
||||
return compiledTemplate(model);
|
||||
}
|
||||
----
|
||||
|
||||
Check out the Spring Framework unit tests,
|
||||
{spring-framework-main-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and
|
||||
{spring-framework-main-code}/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script[resources],
|
||||
for more configuration examples.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-view-httpmessagewriter]]
|
||||
== JSON and XML
|
||||
[.small]#<<web.adoc#mvc-view-jackson, Web MVC>>#
|
||||
|
||||
For <<webflux-multiple-representations>> purposes, it is useful to be able to alternate
|
||||
between rendering a model with an HTML template or as other formats (such as JSON or XML),
|
||||
depending on the content type requested by the client. To support doing so, Spring WebFlux
|
||||
provides the `HttpMessageWriterView`, which you can use to plug in any of the available
|
||||
<<webflux-codecs>> from `spring-web`, such as `Jackson2JsonEncoder`, `Jackson2SmileEncoder`,
|
||||
or `Jaxb2XmlEncoder`.
|
||||
|
||||
Unlike other view technologies, `HttpMessageWriterView` does not require a `ViewResolver`
|
||||
but is instead <<webflux-config-view-resolvers, configured>> as a default view. You can
|
||||
configure one or more such default views, wrapping different `HttpMessageWriter` instances
|
||||
or `Encoder` instances. The one that matches the requested content type is used at runtime.
|
||||
|
||||
In most cases, a model contains multiple attributes. To determine which one to serialize,
|
||||
you can configure `HttpMessageWriterView` with the name of the model attribute to use for
|
||||
rendering. If the model contains only one attribute, that one is used.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,461 +0,0 @@
|
||||
[[webflux-websocket]]
|
||||
= WebSockets
|
||||
[.small]#<<web.adoc#websocket, Same as in the Servlet stack>>#
|
||||
|
||||
This part of the reference documentation covers support for reactive-stack WebSocket
|
||||
messaging.
|
||||
|
||||
include::websocket-intro.adoc[leveloffset=+1]
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server]]
|
||||
== WebSocket API
|
||||
[.small]#<<web.adoc#websocket-server, Same as in the Servlet stack>>#
|
||||
|
||||
The Spring Framework provides a WebSocket API that you can use to write client- and
|
||||
server-side applications that handle WebSocket messages.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server-handler]]
|
||||
=== Server
|
||||
[.small]#<<web.adoc#websocket-server-handler, Same as in the Servlet stack>>#
|
||||
|
||||
To create a WebSocket server, you can first create a `WebSocketHandler`.
|
||||
The following example shows how to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||
import org.springframework.web.reactive.socket.WebSocketSession;
|
||||
|
||||
public class MyWebSocketHandler implements WebSocketHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler
|
||||
import org.springframework.web.reactive.socket.WebSocketSession
|
||||
|
||||
class MyWebSocketHandler : WebSocketHandler {
|
||||
|
||||
override fun handle(session: WebSocketSession): Mono<Void> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Then you can map it to a URL:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
@Bean
|
||||
public HandlerMapping handlerMapping() {
|
||||
Map<String, WebSocketHandler> map = new HashMap<>();
|
||||
map.put("/path", new MyWebSocketHandler());
|
||||
int order = -1; // before annotated controllers
|
||||
|
||||
return new SimpleUrlHandlerMapping(map, order);
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
@Bean
|
||||
fun handlerMapping(): HandlerMapping {
|
||||
val map = mapOf("/path" to MyWebSocketHandler())
|
||||
val order = -1 // before annotated controllers
|
||||
|
||||
return SimpleUrlHandlerMapping(map, order)
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
If using the <<web-reactive.adoc#webflux-config, WebFlux Config>> there is nothing
|
||||
further to do, or otherwise if not using the WebFlux config you'll need to declare a
|
||||
`WebSocketHandlerAdapter` as shown below:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
// ...
|
||||
|
||||
@Bean
|
||||
public WebSocketHandlerAdapter handlerAdapter() {
|
||||
return new WebSocketHandlerAdapter();
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
// ...
|
||||
|
||||
@Bean
|
||||
fun handlerAdapter() = WebSocketHandlerAdapter()
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[webflux-websockethandler]]
|
||||
=== `WebSocketHandler`
|
||||
|
||||
The `handle` method of `WebSocketHandler` takes `WebSocketSession` and returns `Mono<Void>`
|
||||
to indicate when application handling of the session is complete. The session is handled
|
||||
through two streams, one for inbound and one for outbound messages. The following table
|
||||
describes the two methods that handle the streams:
|
||||
|
||||
[options="header"]
|
||||
|===
|
||||
| `WebSocketSession` method | Description
|
||||
|
||||
| `Flux<WebSocketMessage> receive()`
|
||||
| Provides access to the inbound message stream and completes when the connection is closed.
|
||||
|
||||
| `Mono<Void> send(Publisher<WebSocketMessage>)`
|
||||
| Takes a source for outgoing messages, writes the messages, and returns a `Mono<Void>` that
|
||||
completes when the source completes and writing is done.
|
||||
|
||||
|===
|
||||
|
||||
A `WebSocketHandler` must compose the inbound and outbound streams into a unified flow and
|
||||
return a `Mono<Void>` that reflects the completion of that flow. Depending on application
|
||||
requirements, the unified flow completes when:
|
||||
|
||||
* Either the inbound or the outbound message stream completes.
|
||||
* The inbound stream completes (that is, the connection closed), while the outbound stream is infinite.
|
||||
* At a chosen point, through the `close` method of `WebSocketSession`.
|
||||
|
||||
When inbound and outbound message streams are composed together, there is no need to
|
||||
check if the connection is open, since Reactive Streams signals end activity.
|
||||
The inbound stream receives a completion or error signal, and the outbound stream
|
||||
receives a cancellation signal.
|
||||
|
||||
The most basic implementation of a handler is one that handles the inbound stream. The
|
||||
following example shows such an implementation:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
class ExampleHandler implements WebSocketHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
return session.receive() // <1>
|
||||
.doOnNext(message -> {
|
||||
// ... // <2>
|
||||
})
|
||||
.concatMap(message -> {
|
||||
// ... // <3>
|
||||
})
|
||||
.then(); // <4>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Access the stream of inbound messages.
|
||||
<2> Do something with each message.
|
||||
<3> Perform nested asynchronous operations that use the message content.
|
||||
<4> Return a `Mono<Void>` that completes when receiving completes.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class ExampleHandler : WebSocketHandler {
|
||||
|
||||
override fun handle(session: WebSocketSession): Mono<Void> {
|
||||
return session.receive() // <1>
|
||||
.doOnNext {
|
||||
// ... // <2>
|
||||
}
|
||||
.concatMap {
|
||||
// ... // <3>
|
||||
}
|
||||
.then() // <4>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Access the stream of inbound messages.
|
||||
<2> Do something with each message.
|
||||
<3> Perform nested asynchronous operations that use the message content.
|
||||
<4> Return a `Mono<Void>` that completes when receiving completes.
|
||||
|
||||
|
||||
TIP: For nested, asynchronous operations, you may need to call `message.retain()` on underlying
|
||||
servers that use pooled data buffers (for example, Netty). Otherwise, the data buffer may be
|
||||
released before you have had a chance to read the data. For more background, see
|
||||
<<core.adoc#databuffers, Data Buffers and Codecs>>.
|
||||
|
||||
The following implementation combines the inbound and outbound streams:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
class ExampleHandler implements WebSocketHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
|
||||
Flux<WebSocketMessage> output = session.receive() // <1>
|
||||
.doOnNext(message -> {
|
||||
// ...
|
||||
})
|
||||
.concatMap(message -> {
|
||||
// ...
|
||||
})
|
||||
.map(value -> session.textMessage("Echo " + value)); // <2>
|
||||
|
||||
return session.send(output); // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Handle the inbound message stream.
|
||||
<2> Create the outbound message, producing a combined flow.
|
||||
<3> Return a `Mono<Void>` that does not complete while we continue to receive.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class ExampleHandler : WebSocketHandler {
|
||||
|
||||
override fun handle(session: WebSocketSession): Mono<Void> {
|
||||
|
||||
val output = session.receive() // <1>
|
||||
.doOnNext {
|
||||
// ...
|
||||
}
|
||||
.concatMap {
|
||||
// ...
|
||||
}
|
||||
.map { session.textMessage("Echo $it") } // <2>
|
||||
|
||||
return session.send(output) // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Handle the inbound message stream.
|
||||
<2> Create the outbound message, producing a combined flow.
|
||||
<3> Return a `Mono<Void>` that does not complete while we continue to receive.
|
||||
|
||||
|
||||
Inbound and outbound streams can be independent and be joined only for completion,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
class ExampleHandler implements WebSocketHandler {
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
|
||||
Mono<Void> input = session.receive() <1>
|
||||
.doOnNext(message -> {
|
||||
// ...
|
||||
})
|
||||
.concatMap(message -> {
|
||||
// ...
|
||||
})
|
||||
.then();
|
||||
|
||||
Flux<String> source = ... ;
|
||||
Mono<Void> output = session.send(source.map(session::textMessage)); <2>
|
||||
|
||||
return Mono.zip(input, output).then(); <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Handle inbound message stream.
|
||||
<2> Send outgoing messages.
|
||||
<3> Join the streams and return a `Mono<Void>` that completes when either stream ends.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class ExampleHandler : WebSocketHandler {
|
||||
|
||||
override fun handle(session: WebSocketSession): Mono<Void> {
|
||||
|
||||
val input = session.receive() // <1>
|
||||
.doOnNext {
|
||||
// ...
|
||||
}
|
||||
.concatMap {
|
||||
// ...
|
||||
}
|
||||
.then()
|
||||
|
||||
val source: Flux<String> = ...
|
||||
val output = session.send(source.map(session::textMessage)) // <2>
|
||||
|
||||
return Mono.zip(input, output).then() // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Handle inbound message stream.
|
||||
<2> Send outgoing messages.
|
||||
<3> Join the streams and return a `Mono<Void>` that completes when either stream ends.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-databuffer]]
|
||||
=== `DataBuffer`
|
||||
|
||||
`DataBuffer` is the representation for a byte buffer in WebFlux. The Spring Core part of
|
||||
the reference has more on that in the section on
|
||||
<<core#databuffers, Data Buffers and Codecs>>. The key point to understand is that on some
|
||||
servers like Netty, byte buffers are pooled and reference counted, and must be released
|
||||
when consumed to avoid memory leaks.
|
||||
|
||||
When running on Netty, applications must use `DataBufferUtils.retain(dataBuffer)` if they
|
||||
wish to hold on input data buffers in order to ensure they are not released, and
|
||||
subsequently use `DataBufferUtils.release(dataBuffer)` when the buffers are consumed.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server-handshake]]
|
||||
=== Handshake
|
||||
[.small]#<<web.adoc#websocket-server-handshake, Same as in the Servlet stack>>#
|
||||
|
||||
`WebSocketHandlerAdapter` delegates to a `WebSocketService`. By default, that is an instance
|
||||
of `HandshakeWebSocketService`, which performs basic checks on the WebSocket request and
|
||||
then uses `RequestUpgradeStrategy` for the server in use. Currently, there is built-in
|
||||
support for Reactor Netty, Tomcat, Jetty, and Undertow.
|
||||
|
||||
`HandshakeWebSocketService` exposes a `sessionAttributePredicate` property that allows
|
||||
setting a `Predicate<String>` to extract attributes from the `WebSession` and insert them
|
||||
into the attributes of the `WebSocketSession`.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server-config]]
|
||||
=== Server Configuration
|
||||
[.small]#<<web.adoc#websocket-server-runtime-configuration, Same as in the Servlet stack>>#
|
||||
|
||||
The `RequestUpgradeStrategy` for each server exposes configuration specific to the
|
||||
underlying WebSocket server engine. When using the WebFlux Java config you can customize
|
||||
such properties as shown in the corresponding section of the
|
||||
<<web-reactive.adoc#webflux-config-websocket-service, WebFlux Config>>, or otherwise if
|
||||
not using the WebFlux config, use the below:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
@Bean
|
||||
public WebSocketHandlerAdapter handlerAdapter() {
|
||||
return new WebSocketHandlerAdapter(webSocketService());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketService webSocketService() {
|
||||
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
|
||||
strategy.setMaxSessionIdleTimeout(0L);
|
||||
return new HandshakeWebSocketService(strategy);
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
class WebConfig {
|
||||
|
||||
@Bean
|
||||
fun handlerAdapter() =
|
||||
WebSocketHandlerAdapter(webSocketService())
|
||||
|
||||
@Bean
|
||||
fun webSocketService(): WebSocketService {
|
||||
val strategy = TomcatRequestUpgradeStrategy().apply {
|
||||
setMaxSessionIdleTimeout(0L)
|
||||
}
|
||||
return HandshakeWebSocketService(strategy)
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Check the upgrade strategy for your server to see what options are available. Currently,
|
||||
only Tomcat and Jetty expose such options.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-server-cors]]
|
||||
=== CORS
|
||||
[.small]#<<web.adoc#websocket-server-allowed-origins, Same as in the Servlet stack>>#
|
||||
|
||||
The easiest way to configure CORS and restrict access to a WebSocket endpoint is to
|
||||
have your `WebSocketHandler` implement `CorsConfigurationSource` and return a
|
||||
`CorsConfiguration` with allowed origins, headers, and other details. If you cannot do
|
||||
that, you can also set the `corsConfigurations` property on the `SimpleUrlHandler` to
|
||||
specify CORS settings by URL pattern. If both are specified, they are combined by using the
|
||||
`combine` method on `CorsConfiguration`.
|
||||
|
||||
|
||||
|
||||
[[webflux-websocket-client]]
|
||||
=== Client
|
||||
|
||||
Spring WebFlux provides a `WebSocketClient` abstraction with implementations for
|
||||
Reactor Netty, Tomcat, Jetty, Undertow, and standard Java (that is, JSR-356).
|
||||
|
||||
NOTE: The Tomcat client is effectively an extension of the standard Java one with some extra
|
||||
functionality in the `WebSocketSession` handling to take advantage of the Tomcat-specific
|
||||
API to suspend receiving messages for back pressure.
|
||||
|
||||
To start a WebSocket session, you can create an instance of the client and use its `execute`
|
||||
methods:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
WebSocketClient client = new ReactorNettyWebSocketClient();
|
||||
|
||||
URI url = new URI("ws://localhost:8080/path");
|
||||
client.execute(url, session ->
|
||||
session.receive()
|
||||
.doOnNext(System.out::println)
|
||||
.then());
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val client = ReactorNettyWebSocketClient()
|
||||
|
||||
val url = URI("ws://localhost:8080/path")
|
||||
client.execute(url) { session ->
|
||||
session.receive()
|
||||
.doOnNext(::println)
|
||||
.then()
|
||||
}
|
||||
----
|
||||
|
||||
Some clients, such as Jetty, implement `Lifecycle` and need to be stopped and started
|
||||
before you can use them. All clients have constructor options related to configuration
|
||||
of the underlying WebSocket client.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,55 +0,0 @@
|
||||
[[webmvc-client]]
|
||||
= REST Clients
|
||||
|
||||
This section describes options for client-side access to REST endpoints.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-resttemplate]]
|
||||
== `RestTemplate`
|
||||
|
||||
`RestTemplate` is a synchronous client to perform HTTP requests. It is the original
|
||||
Spring REST client and exposes a simple, template-method API over underlying HTTP client
|
||||
libraries.
|
||||
|
||||
NOTE: As of 5.0 the `RestTemplate` is in maintenance mode, with only requests for minor
|
||||
changes and bugs to be accepted. Please, consider using the
|
||||
<<web-reactive.adoc#webflux-client, WebClient>> which offers a more modern API and
|
||||
supports sync, async, and streaming scenarios.
|
||||
|
||||
See <<integration.adoc#rest-client-access, REST Endpoints>> for details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was
|
||||
introduced in 5.0 and offers a modern alternative to the `RestTemplate`, with efficient
|
||||
support for both synchronous and asynchronous, as well as streaming scenarios.
|
||||
|
||||
In contrast to `RestTemplate`, `WebClient` supports the following:
|
||||
|
||||
* Non-blocking I/O.
|
||||
* Reactive Streams back pressure.
|
||||
* High concurrency with fewer hardware resources.
|
||||
* Functional-style, fluent API that takes advantage of Java 8 lambdas.
|
||||
* Synchronous and asynchronous interactions.
|
||||
* Streaming up to or streaming down from a server.
|
||||
|
||||
See <<web-reactive.adoc#webflux-client, WebClient>> for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-http-interface]]
|
||||
== HTTP Interface
|
||||
|
||||
The Spring Frameworks lets you define an HTTP service as a Java interface with HTTP
|
||||
exchange methods. You can then generate a proxy that implements this interface and
|
||||
performs the exchanges. This helps to simplify HTTP remote access and provides additional
|
||||
flexibility for to choose an API style such as synchronous or reactive.
|
||||
|
||||
See <<integration.adoc#rest-http-interface, REST Endpoints>> for details.
|
||||
@@ -1,379 +0,0 @@
|
||||
[[mvc-cors]]
|
||||
= CORS
|
||||
[.small]#<<web-reactive.adoc#webflux-cors, WebFlux>>#
|
||||
|
||||
Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
describes how to do so.
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-intro]]
|
||||
== Introduction
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-intro, WebFlux>>#
|
||||
|
||||
For security reasons, browsers prohibit AJAX calls to resources outside the current origin.
|
||||
For example, you could have your bank account in one tab and evil.com in another. Scripts
|
||||
from evil.com should not be able to make AJAX requests to your bank API with your
|
||||
credentials -- for example withdrawing money from your account!
|
||||
|
||||
Cross-Origin Resource Sharing (CORS) is a https://www.w3.org/TR/cors/[W3C specification]
|
||||
implemented by https://caniuse.com/#feat=cors[most browsers] that lets you specify
|
||||
what kind of cross-domain requests are authorized, rather than using less secure and less
|
||||
powerful workarounds based on IFRAME or JSONP.
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-processing]]
|
||||
== Processing
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-processing, WebFlux>>#
|
||||
|
||||
The CORS specification distinguishes between preflight, simple, and actual requests.
|
||||
To learn how CORS works, you can read
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[this article], among
|
||||
many others, or see the specification for more details.
|
||||
|
||||
Spring MVC `HandlerMapping` implementations provide built-in support for CORS. After successfully
|
||||
mapping a request to a handler, `HandlerMapping` implementations check the CORS configuration for the
|
||||
given request and handler and take further actions. Preflight requests are handled
|
||||
directly, while simple and actual CORS requests are intercepted, validated, and have
|
||||
required CORS response headers set.
|
||||
|
||||
In order to enable cross-origin requests (that is, the `Origin` header is present and
|
||||
differs from the host of the request), you need to have some explicitly declared CORS
|
||||
configuration. If no matching CORS configuration is found, preflight requests are
|
||||
rejected. No CORS headers are added to the responses of simple and actual CORS requests
|
||||
and, consequently, browsers reject them.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{api-spring-framework}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
|
||||
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
|
||||
use the MVC Java configuration or the XML namespace to declare such mappings, which results
|
||||
in a single global map being passed to all `HandlerMapping` instances.
|
||||
|
||||
You can combine global CORS configuration at the `HandlerMapping` level with more
|
||||
fine-grained, handler-level CORS configuration. For example, annotated controllers can use
|
||||
class- or method-level `@CrossOrigin` annotations (other handlers can implement
|
||||
`CorsConfigurationSource`).
|
||||
|
||||
The rules for combining global and local configuration are generally additive -- for example,
|
||||
all global and all local origins. For those attributes where only a single value can be
|
||||
accepted, e.g. `allowCredentials` and `maxAge`, the local overrides the global value. See
|
||||
{api-spring-framework}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
for more details.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
To learn more from the source or make advanced customizations, check the code behind:
|
||||
|
||||
* `CorsConfiguration`
|
||||
* `CorsProcessor`, `DefaultCorsProcessor`
|
||||
* `AbstractHandlerMapping`
|
||||
====
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-controller]]
|
||||
== `@CrossOrigin`
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-controller, WebFlux>>#
|
||||
|
||||
The {api-spring-framework}/web/bind/annotation/CrossOrigin.html[`@CrossOrigin`]
|
||||
annotation enables cross-origin requests on annotated controller methods,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin
|
||||
@GetMapping("/{id}")
|
||||
public Account retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
class AccountController {
|
||||
|
||||
@CrossOrigin
|
||||
@GetMapping("/{id}")
|
||||
fun retrieve(@PathVariable id: Long): Account {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
By default, `@CrossOrigin` allows:
|
||||
|
||||
* All origins.
|
||||
* All headers.
|
||||
* All HTTP methods to which the controller method is mapped.
|
||||
|
||||
`allowCredentials` is not enabled by default, since that establishes a trust level
|
||||
that exposes sensitive user-specific information (such as cookies and CSRF tokens) and
|
||||
should only be used where appropriate. When it is enabled either `allowOrigins` must be
|
||||
set to one or more specific domain (but not the special value `"*"`) or alternatively
|
||||
the `allowOriginPatterns` property may be used to match to a dynamic set of origins.
|
||||
|
||||
`maxAge` is set to 30 minutes.
|
||||
|
||||
`@CrossOrigin` is supported at the class level, too, and is inherited by all methods,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Account retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@CrossOrigin(origins = ["https://domain2.com"], maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
class AccountController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun retrieve(@PathVariable id: Long): Account {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
You can use `@CrossOrigin` at both the class level and the method level,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@CrossOrigin(maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
public class AccountController {
|
||||
|
||||
@CrossOrigin("https://domain2.com")
|
||||
@GetMapping("/{id}")
|
||||
public Account retrieve(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void remove(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@CrossOrigin(maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/account")
|
||||
class AccountController {
|
||||
|
||||
@CrossOrigin("https://domain2.com")
|
||||
@GetMapping("/{id}")
|
||||
fun retrieve(@PathVariable id: Long): Account {
|
||||
// ...
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-global]]
|
||||
== Global Configuration
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-global, WebFlux>>#
|
||||
|
||||
In addition to fine-grained, controller method level configuration, you probably want to
|
||||
define some global CORS configuration, too. You can set URL-based `CorsConfiguration`
|
||||
mappings individually on any `HandlerMapping`. Most applications, however, use the
|
||||
MVC Java configuration or the MVC XML namespace to do that.
|
||||
|
||||
By default, global configuration enables the following:
|
||||
|
||||
* All origins.
|
||||
* All headers.
|
||||
* `GET`, `HEAD`, and `POST` methods.
|
||||
|
||||
|
||||
`allowCredentials` is not enabled by default, since that establishes a trust level
|
||||
that exposes sensitive user-specific information (such as cookies and CSRF tokens) and
|
||||
should only be used where appropriate. When it is enabled either `allowOrigins` must be
|
||||
set to one or more specific domain (but not the special value `"*"`) or alternatively
|
||||
the `allowOriginPatterns` property may be used to match to a dynamic set of origins.
|
||||
|
||||
`maxAge` is set to 30 minutes.
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-global-java]]
|
||||
=== Java Configuration
|
||||
[.small]#<<web-reactive.adoc#webflux-cors-global, WebFlux>>#
|
||||
|
||||
To enable CORS in the MVC Java config, you can use the `CorsRegistry` callback,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("https://domain2.com")
|
||||
.allowedMethods("PUT", "DELETE")
|
||||
.allowedHeaders("header1", "header2", "header3")
|
||||
.exposedHeaders("header1", "header2")
|
||||
.allowCredentials(true).maxAge(3600);
|
||||
|
||||
// Add more mappings...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
class WebConfig : WebMvcConfigurer {
|
||||
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("https://domain2.com")
|
||||
.allowedMethods("PUT", "DELETE")
|
||||
.allowedHeaders("header1", "header2", "header3")
|
||||
.exposedHeaders("header1", "header2")
|
||||
.allowCredentials(true).maxAge(3600)
|
||||
|
||||
// Add more mappings...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-global-xml]]
|
||||
=== XML Configuration
|
||||
|
||||
To enable CORS in the XML namespace, you can use the `<mvc:cors>` element,
|
||||
as the following example shows:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim"]
|
||||
----
|
||||
<mvc:cors>
|
||||
|
||||
<mvc:mapping path="/api/**"
|
||||
allowed-origins="https://domain1.com, https://domain2.com"
|
||||
allowed-methods="GET, PUT"
|
||||
allowed-headers="header1, header2, header3"
|
||||
exposed-headers="header1, header2" allow-credentials="true"
|
||||
max-age="123" />
|
||||
|
||||
<mvc:mapping path="/resources/**"
|
||||
allowed-origins="https://domain1.com" />
|
||||
|
||||
</mvc:cors>
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-cors-filter]]
|
||||
== CORS Filter
|
||||
[.small]#<<webflux-cors.adoc#webflux-cors-webfilter, WebFlux>>#
|
||||
|
||||
You can apply CORS support through the built-in
|
||||
{api-spring-framework}/web/filter/CorsFilter.html[`CorsFilter`].
|
||||
|
||||
NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring
|
||||
Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for
|
||||
CORS.
|
||||
|
||||
To configure the filter, pass a `CorsConfigurationSource` to its constructor, as the
|
||||
following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim",role="primary"]
|
||||
.Java
|
||||
----
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
|
||||
// Possibly...
|
||||
// config.applyPermitDefaultValues()
|
||||
|
||||
config.setAllowCredentials(true);
|
||||
config.addAllowedOrigin("https://domain1.com");
|
||||
config.addAllowedHeader("*");
|
||||
config.addAllowedMethod("*");
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
CorsFilter filter = new CorsFilter(source);
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val config = CorsConfiguration()
|
||||
|
||||
// Possibly...
|
||||
// config.applyPermitDefaultValues()
|
||||
|
||||
config.allowCredentials = true
|
||||
config.addAllowedOrigin("https://domain1.com")
|
||||
config.addAllowedHeader("*")
|
||||
config.addAllowedMethod("*")
|
||||
|
||||
val source = UrlBasedCorsConfigurationSource()
|
||||
source.registerCorsConfiguration("/**", config)
|
||||
|
||||
val filter = CorsFilter(source)
|
||||
----
|
||||
@@ -1,876 +0,0 @@
|
||||
[[webmvc-fn]]
|
||||
= Functional Endpoints
|
||||
[.small]#<<web-reactive.adoc#webflux-fn, WebFlux>>#
|
||||
|
||||
Spring Web MVC includes WebMvc.fn, a lightweight functional programming model in which functions
|
||||
are used to route and handle requests and contracts are designed for immutability.
|
||||
It is an alternative to the annotation-based programming model but otherwise runs on
|
||||
the same <<web#mvc-servlet>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-overview]]
|
||||
== Overview
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-overview, WebFlux>>#
|
||||
|
||||
In WebMvc.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes
|
||||
`ServerRequest` and returns a `ServerResponse`.
|
||||
Both the request and the response object have immutable contracts that offer JDK 8-friendly
|
||||
access to the HTTP request and response.
|
||||
`HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the
|
||||
annotation-based programming model.
|
||||
|
||||
Incoming requests are routed to a handler function with a `RouterFunction`: a function that
|
||||
takes `ServerRequest` and returns an optional `HandlerFunction` (i.e. `Optional<HandlerFunction>`).
|
||||
When the router function matches, a handler function is returned; otherwise an empty Optional.
|
||||
`RouterFunction` is the equivalent of a `@RequestMapping` annotation, but with the major
|
||||
difference that router functions provide not just data, but also behavior.
|
||||
|
||||
`RouterFunctions.route()` provides a router builder that facilitates the creation of routers,
|
||||
as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.servlet.function.RequestPredicates.*;
|
||||
import static org.springframework.web.servlet.function.RouterFunctions.route;
|
||||
|
||||
PersonRepository repository = ...
|
||||
PersonHandler handler = new PersonHandler(repository);
|
||||
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST("/person", handler::createPerson)
|
||||
.build();
|
||||
|
||||
|
||||
public class PersonHandler {
|
||||
|
||||
// ...
|
||||
|
||||
public ServerResponse listPeople(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public ServerResponse createPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
|
||||
public ServerResponse getPerson(ServerRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val repository: PersonRepository = ...
|
||||
val handler = PersonHandler(repository)
|
||||
|
||||
val route = router { // <1>
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/person/{id}", handler::getPerson)
|
||||
GET("/person", handler::listPeople)
|
||||
}
|
||||
POST("/person", handler::createPerson)
|
||||
}
|
||||
|
||||
|
||||
class PersonHandler(private val repository: PersonRepository) {
|
||||
|
||||
// ...
|
||||
|
||||
fun listPeople(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
|
||||
fun createPerson(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
|
||||
fun getPerson(request: ServerRequest): ServerResponse {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create router using the router DSL.
|
||||
|
||||
|
||||
If you register the `RouterFunction` as a bean, for instance by exposing it in a
|
||||
`@Configuration` class, it will be auto-detected by the servlet, as explained in <<webmvc-fn-running>>.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-handler-functions]]
|
||||
== HandlerFunction
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-handler-functions, WebFlux>>#
|
||||
|
||||
`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly
|
||||
access to the HTTP request and response, including headers, body, method, and status code.
|
||||
|
||||
|
||||
[[webmvc-fn-request]]
|
||||
=== ServerRequest
|
||||
|
||||
`ServerRequest` provides access to the HTTP method, URI, headers, and query parameters,
|
||||
while access to the body is provided through the `body` methods.
|
||||
|
||||
The following example extracts the request body to a `String`:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
String string = request.body(String.class);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val string = request.body<String>()
|
||||
----
|
||||
|
||||
|
||||
The following example extracts the body to a `List<Person>`,
|
||||
where `Person` objects are decoded from a serialized form, such as JSON or XML:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val people = request.body<Person>()
|
||||
----
|
||||
|
||||
The following example shows how to access parameters:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
MultiValueMap<String, String> params = request.params();
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val map = request.params()
|
||||
----
|
||||
|
||||
|
||||
[[webmvc-fn-response]]
|
||||
=== ServerResponse
|
||||
|
||||
`ServerResponse` provides access to the HTTP response and, since it is immutable, you can use
|
||||
a `build` method to create it. You can use the builder to set the response status, to add response
|
||||
headers, or to provide a body. The following example creates a 200 (OK) response with JSON
|
||||
content:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Person person = ...
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val person: Person = ...
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
|
||||
----
|
||||
|
||||
The following example shows how to build a 201 (CREATED) response with a `Location` header and no body:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
URI location = ...
|
||||
ServerResponse.created(location).build();
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val location: URI = ...
|
||||
ServerResponse.created(location).build()
|
||||
----
|
||||
|
||||
You can also use an asynchronous result as the body, in the form of a `CompletableFuture`,
|
||||
`Publisher`, or any other type supported by the `ReactiveAdapterRegistry`. For instance:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
|
||||
----
|
||||
[source,kotlin,role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val person = webClient.get().retrieve().awaitBody<Person>()
|
||||
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
|
||||
----
|
||||
|
||||
If not just the body, but also the status or headers are based on an asynchronous type,
|
||||
you can use the static `async` method on `ServerResponse`, which
|
||||
accepts `CompletableFuture<ServerResponse>`, `Publisher<ServerResponse>`, or
|
||||
any other asynchronous type supported by the `ReactiveAdapterRegistry`. For instance:
|
||||
|
||||
[source,java,role="primary"]
|
||||
.Java
|
||||
----
|
||||
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
|
||||
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
|
||||
ServerResponse.async(asyncResponse);
|
||||
----
|
||||
|
||||
https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the
|
||||
static `sse` method on `ServerResponse`. The builder provided by that method
|
||||
allows you to send Strings, or other objects as JSON. For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
public RouterFunction<ServerResponse> sse() {
|
||||
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
|
||||
// Save the sseBuilder object somewhere..
|
||||
}));
|
||||
}
|
||||
|
||||
// In some other thread, sending a String
|
||||
sseBuilder.send("Hello world");
|
||||
|
||||
// Or an object, which will be transformed into JSON
|
||||
Person person = ...
|
||||
sseBuilder.send(person);
|
||||
|
||||
// Customize the event by using the other methods
|
||||
sseBuilder.id("42")
|
||||
.event("sse event")
|
||||
.data(person);
|
||||
|
||||
// and done at some point
|
||||
sseBuilder.complete();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
fun sse(): RouterFunction<ServerResponse> = router {
|
||||
GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
|
||||
// Save the sseBuilder object somewhere..
|
||||
}
|
||||
}
|
||||
|
||||
// In some other thread, sending a String
|
||||
sseBuilder.send("Hello world")
|
||||
|
||||
// Or an object, which will be transformed into JSON
|
||||
val person = ...
|
||||
sseBuilder.send(person)
|
||||
|
||||
// Customize the event by using the other methods
|
||||
sseBuilder.id("42")
|
||||
.event("sse event")
|
||||
.data(person)
|
||||
|
||||
// and done at some point
|
||||
sseBuilder.complete()
|
||||
----
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-handler-classes]]
|
||||
=== Handler Classes
|
||||
|
||||
We can write a handler function as a lambda, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
HandlerFunction<ServerResponse> helloWorld =
|
||||
request -> ServerResponse.ok().body("Hello World");
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
val helloWorld: (ServerRequest) -> ServerResponse =
|
||||
{ ServerResponse.ok().body("Hello World") }
|
||||
----
|
||||
|
||||
That is convenient, but in an application we need multiple functions, and multiple inline
|
||||
lambda's can get messy.
|
||||
Therefore, it is useful to group related handler functions together into a handler class, which
|
||||
has a similar role as `@Controller` in an annotation-based application.
|
||||
For example, the following class exposes a reactive `Person` repository:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
|
||||
|
||||
public class PersonHandler {
|
||||
|
||||
private final PersonRepository repository;
|
||||
|
||||
public PersonHandler(PersonRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public ServerResponse listPeople(ServerRequest request) { // <1>
|
||||
List<Person> people = repository.allPeople();
|
||||
return ok().contentType(APPLICATION_JSON).body(people);
|
||||
}
|
||||
|
||||
public ServerResponse createPerson(ServerRequest request) throws Exception { // <2>
|
||||
Person person = request.body(Person.class);
|
||||
repository.savePerson(person);
|
||||
return ok().build();
|
||||
}
|
||||
|
||||
public ServerResponse getPerson(ServerRequest request) { // <3>
|
||||
int personId = Integer.parseInt(request.pathVariable("id"));
|
||||
Person person = repository.getPerson(personId);
|
||||
if (person != null) {
|
||||
return ok().contentType(APPLICATION_JSON).body(person);
|
||||
}
|
||||
else {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as
|
||||
JSON.
|
||||
<2> `createPerson` is a handler function that stores a new `Person` contained in the request body.
|
||||
<3> `getPerson` is a handler function that returns a single person, identified by the `id` path
|
||||
variable. We retrieve that `Person` from the repository and create a JSON response, if it is
|
||||
found. If it is not found, we return a 404 Not Found response.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class PersonHandler(private val repository: PersonRepository) {
|
||||
|
||||
fun listPeople(request: ServerRequest): ServerResponse { // <1>
|
||||
val people: List<Person> = repository.allPeople()
|
||||
return ok().contentType(APPLICATION_JSON).body(people);
|
||||
}
|
||||
|
||||
fun createPerson(request: ServerRequest): ServerResponse { // <2>
|
||||
val person = request.body<Person>()
|
||||
repository.savePerson(person)
|
||||
return ok().build()
|
||||
}
|
||||
|
||||
fun getPerson(request: ServerRequest): ServerResponse { // <3>
|
||||
val personId = request.pathVariable("id").toInt()
|
||||
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
|
||||
?: ServerResponse.notFound().build()
|
||||
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as
|
||||
JSON.
|
||||
<2> `createPerson` is a handler function that stores a new `Person` contained in the request body.
|
||||
<3> `getPerson` is a handler function that returns a single person, identified by the `id` path
|
||||
variable. We retrieve that `Person` from the repository and create a JSON response, if it is
|
||||
found. If it is not found, we return a 404 Not Found response.
|
||||
|
||||
|
||||
[[webmvc-fn-handler-validation]]
|
||||
=== Validation
|
||||
|
||||
A functional endpoint can use Spring's <<core.adoc#validation, validation facilities>> to
|
||||
apply validation to the request body. For example, given a custom Spring
|
||||
<<core.adoc#validation, Validator>> implementation for a `Person`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
public class PersonHandler {
|
||||
|
||||
private final Validator validator = new PersonValidator(); // <1>
|
||||
|
||||
// ...
|
||||
|
||||
public ServerResponse createPerson(ServerRequest request) {
|
||||
Person person = request.body(Person.class);
|
||||
validate(person); // <2>
|
||||
repository.savePerson(person);
|
||||
return ok().build();
|
||||
}
|
||||
|
||||
private void validate(Person person) {
|
||||
Errors errors = new BeanPropertyBindingResult(person, "person");
|
||||
validator.validate(person, errors);
|
||||
if (errors.hasErrors()) {
|
||||
throw new ServerWebInputException(errors.toString()); // <3>
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create `Validator` instance.
|
||||
<2> Apply validation.
|
||||
<3> Raise exception for a 400 response.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
class PersonHandler(private val repository: PersonRepository) {
|
||||
|
||||
private val validator = PersonValidator() // <1>
|
||||
|
||||
// ...
|
||||
|
||||
fun createPerson(request: ServerRequest): ServerResponse {
|
||||
val person = request.body<Person>()
|
||||
validate(person) // <2>
|
||||
repository.savePerson(person)
|
||||
return ok().build()
|
||||
}
|
||||
|
||||
private fun validate(person: Person) {
|
||||
val errors: Errors = BeanPropertyBindingResult(person, "person")
|
||||
validator.validate(person, errors)
|
||||
if (errors.hasErrors()) {
|
||||
throw ServerWebInputException(errors.toString()) // <3>
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Create `Validator` instance.
|
||||
<2> Apply validation.
|
||||
<3> Raise exception for a 400 response.
|
||||
|
||||
Handlers can also use the standard bean validation API (JSR-303) by creating and injecting
|
||||
a global `Validator` instance based on `LocalValidatorFactoryBean`.
|
||||
See <<core.adoc#validation-beanvalidation, Spring Validation>>.
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-router-functions]]
|
||||
== `RouterFunction`
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-router-functions, WebFlux>>#
|
||||
|
||||
Router functions are used to route the requests to the corresponding `HandlerFunction`.
|
||||
Typically, you do not write router functions yourself, but rather use a method on the
|
||||
`RouterFunctions` utility class to create one.
|
||||
`RouterFunctions.route()` (no parameters) provides you with a fluent builder for creating a router
|
||||
function, whereas `RouterFunctions.route(RequestPredicate, HandlerFunction)` offers a direct way
|
||||
to create a router.
|
||||
|
||||
Generally, it is recommended to use the `route()` builder, as it provides
|
||||
convenient short-cuts for typical mapping scenarios without requiring hard-to-discover
|
||||
static imports.
|
||||
For instance, the router function builder offers the method `GET(String, HandlerFunction)` to create a mapping for GET requests; and `POST(String, HandlerFunction)` for POSTs.
|
||||
|
||||
Besides HTTP method-based mapping, the route builder offers a way to introduce additional
|
||||
predicates when mapping to requests.
|
||||
For each HTTP method there is an overloaded variant that takes a `RequestPredicate` as a
|
||||
parameter, through which additional constraints can be expressed.
|
||||
|
||||
|
||||
[[webmvc-fn-predicates]]
|
||||
=== Predicates
|
||||
|
||||
You can write your own `RequestPredicate`, but the `RequestPredicates` utility class
|
||||
offers commonly used implementations, based on the request path, HTTP method, content-type,
|
||||
and so on.
|
||||
The following example uses a request predicate to create a constraint based on the `Accept`
|
||||
header:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = RouterFunctions.route()
|
||||
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
|
||||
request -> ServerResponse.ok().body("Hello World")).build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
GET("/hello-world", accept(TEXT_PLAIN)) {
|
||||
ServerResponse.ok().body("Hello World")
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
You can compose multiple request predicates together by using:
|
||||
|
||||
* `RequestPredicate.and(RequestPredicate)` -- both must match.
|
||||
* `RequestPredicate.or(RequestPredicate)` -- either can match.
|
||||
|
||||
Many of the predicates from `RequestPredicates` are composed.
|
||||
For example, `RequestPredicates.GET(String)` is composed from `RequestPredicates.method(HttpMethod)`
|
||||
and `RequestPredicates.path(String)`.
|
||||
The example shown above also uses two request predicates, as the builder uses
|
||||
`RequestPredicates.GET` internally, and composes that with the `accept` predicate.
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-routes]]
|
||||
=== Routes
|
||||
|
||||
Router functions are evaluated in order: if the first route does not match, the
|
||||
second is evaluated, and so on.
|
||||
Therefore, it makes sense to declare more specific routes before general ones.
|
||||
This is also important when registering router functions as Spring beans, as will
|
||||
be described later.
|
||||
Note that this behavior is different from the annotation-based programming model, where the
|
||||
"most specific" controller method is picked automatically.
|
||||
|
||||
When using the router function builder, all defined routes are composed into one
|
||||
`RouterFunction` that is returned from `build()`.
|
||||
There are also other ways to compose multiple router functions together:
|
||||
|
||||
* `add(RouterFunction)` on the `RouterFunctions.route()` builder
|
||||
* `RouterFunction.and(RouterFunction)`
|
||||
* `RouterFunction.andRoute(RequestPredicate, HandlerFunction)` -- shortcut for
|
||||
`RouterFunction.and()` with nested `RouterFunctions.route()`.
|
||||
|
||||
The following example shows the composition of four routes:
|
||||
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.springframework.web.servlet.function.RequestPredicates.*;
|
||||
|
||||
PersonRepository repository = ...
|
||||
PersonHandler handler = new PersonHandler(repository);
|
||||
|
||||
RouterFunction<ServerResponse> otherRoute = ...
|
||||
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
|
||||
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
|
||||
.POST("/person", handler::createPerson) // <3>
|
||||
.add(otherRoute) // <4>
|
||||
.build();
|
||||
----
|
||||
<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to
|
||||
`PersonHandler.getPerson`
|
||||
<2> `GET /person` with an `Accept` header that matches JSON is routed to
|
||||
`PersonHandler.listPeople`
|
||||
<3> `POST /person` with no additional predicates is mapped to
|
||||
`PersonHandler.createPerson`, and
|
||||
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.http.MediaType.APPLICATION_JSON
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val repository: PersonRepository = ...
|
||||
val handler = PersonHandler(repository);
|
||||
|
||||
val otherRoute = router { }
|
||||
|
||||
val route = router {
|
||||
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
|
||||
GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
|
||||
POST("/person", handler::createPerson) // <3>
|
||||
}.and(otherRoute) // <4>
|
||||
----
|
||||
<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to
|
||||
`PersonHandler.getPerson`
|
||||
<2> `GET /person` with an `Accept` header that matches JSON is routed to
|
||||
`PersonHandler.listPeople`
|
||||
<3> `POST /person` with no additional predicates is mapped to
|
||||
`PersonHandler.createPerson`, and
|
||||
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built.
|
||||
|
||||
|
||||
=== Nested Routes
|
||||
|
||||
It is common for a group of router functions to have a shared predicate, for instance a shared
|
||||
path.
|
||||
In the example above, the shared predicate would be a path predicate that matches `/person`,
|
||||
used by three of the routes.
|
||||
When using annotations, you would remove this duplication by using a type-level `@RequestMapping`
|
||||
annotation that maps to `/person`.
|
||||
In WebMvc.fn, path predicates can be shared through the `path` method on the router function builder.
|
||||
For instance, the last few lines of the example above can be improved in the following way by using nested routes:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", builder -> builder // <1>
|
||||
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
.GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
.POST(handler::createPerson))
|
||||
.build();
|
||||
----
|
||||
<1> Note that second parameter of `path` is a consumer that takes the router builder.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
"/person".nest { // <1>
|
||||
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
|
||||
GET(accept(APPLICATION_JSON), handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Using `nest` DSL.
|
||||
|
||||
Though path-based nesting is the most common, you can nest on any kind of predicate by using
|
||||
the `nest` method on the builder.
|
||||
The above still contains some duplication in the form of the shared `Accept`-header predicate.
|
||||
We can further improve by using the `nest` method together with `accept`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(handler::createPerson))
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
"/person".nest {
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[[webmvc-fn-running]]
|
||||
== Running a Server
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-running, WebFlux>>#
|
||||
|
||||
You typically run router functions in a <<web.adoc#mvc-servlet, `DispatcherHandler`>>-based setup through the
|
||||
<<web.adoc#mvc-config>>, which uses Spring configuration to declare the
|
||||
components required to process requests. The MVC Java configuration declares the following
|
||||
infrastructure components to support functional endpoints:
|
||||
|
||||
* `RouterFunctionMapping`: Detects one or more `RouterFunction<?>` beans in the Spring
|
||||
configuration, <<core.adoc#beans-factory-ordered, orders them>>, combines them through
|
||||
`RouterFunction.andOther`, and routes requests to the resulting composed `RouterFunction`.
|
||||
* `HandlerFunctionAdapter`: Simple adapter that lets `DispatcherHandler` invoke
|
||||
a `HandlerFunction` that was mapped to a request.
|
||||
|
||||
The preceding components let functional endpoints fit within the `DispatcherServlet` request
|
||||
processing lifecycle and also (potentially) run side by side with annotated controllers, if
|
||||
any are declared. It is also how functional endpoints are enabled by the Spring Boot Web
|
||||
starter.
|
||||
|
||||
The following example shows a WebFlux Java configuration:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
@Configuration
|
||||
@EnableMvc
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public RouterFunction<?> routerFunctionA() {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<?> routerFunctionB() {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
// configure message conversion...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
// configure CORS...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
// configure view resolution for HTML rendering...
|
||||
}
|
||||
}
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
@Configuration
|
||||
@EnableMvc
|
||||
class WebConfig : WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
fun routerFunctionA(): RouterFunction<*> {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun routerFunctionB(): RouterFunction<*> {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
|
||||
// configure message conversion...
|
||||
}
|
||||
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
// configure CORS...
|
||||
}
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
// configure view resolution for HTML rendering...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-fn-handler-filter-function]]
|
||||
== Filtering Handler Functions
|
||||
[.small]#<<web-reactive.adoc#webflux-fn-handler-filter-function, WebFlux>>#
|
||||
|
||||
You can filter handler functions by using the `before`, `after`, or `filter` methods on the routing
|
||||
function builder.
|
||||
With annotations, you can achieve similar functionality by using `@ControllerAdvice`, a `ServletFilter`, or both.
|
||||
The filter will apply to all routes that are built by the builder.
|
||||
This means that filters defined in nested routes do not apply to "top-level" routes.
|
||||
For instance, consider the following example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople)
|
||||
.before(request -> ServerRequest.from(request) // <1>
|
||||
.header("X-RequestHeader", "Value")
|
||||
.build()))
|
||||
.POST(handler::createPerson))
|
||||
.after((request, response) -> logResponse(response)) // <2>
|
||||
.build();
|
||||
----
|
||||
<1> The `before` filter that adds a custom request header is only applied to the two GET routes.
|
||||
<2> The `after` filter that logs the response is applied to all routes, including the nested ones.
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val route = router {
|
||||
"/person".nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET(handler::listPeople)
|
||||
before { // <1>
|
||||
ServerRequest.from(it)
|
||||
.header("X-RequestHeader", "Value").build()
|
||||
}
|
||||
}
|
||||
POST(handler::createPerson)
|
||||
after { _, response -> // <2>
|
||||
logResponse(response)
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> The `before` filter that adds a custom request header is only applied to the two GET routes.
|
||||
<2> The `after` filter that logs the response is applied to all routes, including the nested ones.
|
||||
|
||||
|
||||
The `filter` method on the router builder takes a `HandlerFilterFunction`: a
|
||||
function that takes a `ServerRequest` and `HandlerFunction` and returns a `ServerResponse`.
|
||||
The handler function parameter represents the next element in the chain.
|
||||
This is typically the handler that is routed to, but it can also be another
|
||||
filter if multiple are applied.
|
||||
|
||||
Now we can add a simple security filter to our route, assuming that we have a `SecurityManager` that
|
||||
can determine whether a particular path is allowed.
|
||||
The following example shows how to do so:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
.Java
|
||||
----
|
||||
SecurityManager securityManager = ...
|
||||
|
||||
RouterFunction<ServerResponse> route = route()
|
||||
.path("/person", b1 -> b1
|
||||
.nest(accept(APPLICATION_JSON), b2 -> b2
|
||||
.GET("/{id}", handler::getPerson)
|
||||
.GET(handler::listPeople))
|
||||
.POST(handler::createPerson))
|
||||
.filter((request, next) -> {
|
||||
if (securityManager.allowAccessTo(request.path())) {
|
||||
return next.handle(request);
|
||||
}
|
||||
else {
|
||||
return ServerResponse.status(UNAUTHORIZED).build();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
----
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
.Kotlin
|
||||
----
|
||||
import org.springframework.web.servlet.function.router
|
||||
|
||||
val securityManager: SecurityManager = ...
|
||||
|
||||
val route = router {
|
||||
("/person" and accept(APPLICATION_JSON)).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
POST(handler::createPerson)
|
||||
filter { request, next ->
|
||||
if (securityManager.allowAccessTo(request.path())) {
|
||||
next(request)
|
||||
}
|
||||
else {
|
||||
status(UNAUTHORIZED).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional.
|
||||
We only let the handler function be run when access is allowed.
|
||||
|
||||
Besides using the `filter` method on the router function builder, it is possible to apply a
|
||||
filter to an existing router function via `RouterFunction.filter(HandlerFilterFunction)`.
|
||||
|
||||
NOTE: CORS support for functional endpoints is provided through a dedicated
|
||||
<<webmvc-cors.adoc#mvc-cors-filter, `CorsFilter`>>.
|
||||
File diff suppressed because it is too large
Load Diff
-14
@@ -1,14 +0,0 @@
|
||||
Spring Framework version ${version}
|
||||
=====================================================================================
|
||||
|
||||
To find out what has changed since earlier releases, see the release notes at
|
||||
https://github.com/spring-projects/spring-framework/releases.
|
||||
|
||||
Please consult the documentation located within the 'docs/spring-framework-reference'
|
||||
directory of this release and also visit the official Spring Framework home at
|
||||
https://spring.io/projects/spring-framework.
|
||||
|
||||
There you will find links to the issue tracker and other resources.
|
||||
|
||||
See https://github.com/spring-projects/spring-framework#readme for additional
|
||||
information including instructions on building from source.
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.core.aot.hints.importruntimehints;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ImportRuntimeHints(SpellCheckService.SpellCheckServiceRuntimeHints.class)
|
||||
public class SpellCheckService {
|
||||
|
||||
public void loadDictionary(Locale locale) {
|
||||
ClassPathResource resource = new ClassPathResource("dicts/" + locale.getLanguage() + ".txt");
|
||||
//...
|
||||
}
|
||||
|
||||
static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
hints.resources().registerPattern("dicts/*");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.core.aot.hints.testing;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
public class SampleReflection {
|
||||
|
||||
private final Log logger = LogFactory.getLog(SampleReflection.class);
|
||||
|
||||
public void performReflection() {
|
||||
try {
|
||||
Class<?> springVersion = ClassUtils.forName("org.springframework.core.SpringVersion", null);
|
||||
Method getVersion = ClassUtils.getMethod(springVersion, "getVersion");
|
||||
String version = (String) getVersion.invoke(null);
|
||||
logger.info("Spring version:" + version);
|
||||
}
|
||||
catch (Exception exc) {
|
||||
logger.error("reflection failed", exc);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.core.aot.hints.testing;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
|
||||
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
|
||||
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
|
||||
import org.springframework.core.SpringVersion;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
// @EnabledIfRuntimeHintsAgent signals that the annotated test class or test
|
||||
// method is only enabled if the RuntimeHintsAgent is loaded on the current JVM.
|
||||
// It also tags tests with the "RuntimeHints" JUnit tag.
|
||||
@EnabledIfRuntimeHintsAgent
|
||||
class SampleReflectionRuntimeHintsTests {
|
||||
|
||||
@Test
|
||||
void shouldRegisterReflectionHints() {
|
||||
RuntimeHints runtimeHints = new RuntimeHints();
|
||||
// Call a RuntimeHintsRegistrar that contributes hints like:
|
||||
runtimeHints.reflection().registerType(SpringVersion.class, typeHint -> {
|
||||
typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE);
|
||||
});
|
||||
|
||||
// Invoke the relevant piece of code we want to test within a recording lambda
|
||||
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
|
||||
SampleReflection sample = new SampleReflection();
|
||||
sample.performReflection();
|
||||
});
|
||||
// assert that the recorded invocations are covered by the contributed hints
|
||||
assertThat(invocations).match(runtimeHints);
|
||||
}
|
||||
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.core.aot.hints.testing;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class SpellCheckServiceTests {
|
||||
|
||||
// tag::hintspredicates[]
|
||||
@Test
|
||||
void shouldRegisterResourceHints() {
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
new SpellCheckServiceRuntimeHints().registerHints(hints, getClass().getClassLoader());
|
||||
assertThat(RuntimeHintsPredicates.resource().forResource("dicts/en.txt"))
|
||||
.accepts(hints);
|
||||
}
|
||||
// end::hintspredicates[]
|
||||
|
||||
// Copied here because it is package private in SpellCheckService
|
||||
static class SpellCheckServiceRuntimeHints implements RuntimeHintsRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
|
||||
hints.resources().registerPattern("dicts/*");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.core.aot.refresh;
|
||||
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
public class AotProcessingSample {
|
||||
|
||||
public void createAotContext() {
|
||||
// tag::aotcontext[]
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.register(MyApplication.class);
|
||||
context.refreshForAotProcessing(hints);
|
||||
// end::aotcontext[]
|
||||
}
|
||||
|
||||
// tag::myapplication[]
|
||||
@Configuration(proxyBeanMethods=false)
|
||||
@ComponentScan
|
||||
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
|
||||
public class MyApplication {
|
||||
}
|
||||
// end::myapplication[]
|
||||
|
||||
class DataSourceConfiguration {
|
||||
}
|
||||
|
||||
class ContainerConfiguration {
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user