Compare commits

..

1 Commits

Author SHA1 Message Date
Spring Buildmaster 4c083093d8 Release version 5.2.0.M2 2019-05-09 16:36:48 +00:00
7180 changed files with 237204 additions and 372150 deletions
-16
View File
@@ -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
+2 -2
View File
@@ -1,5 +1,5 @@
<!--
!!! For Security Vulnerabilities, please go to https://spring.io/security-policy !!!
!!! For Security Vulnerabilities, please go to https://pivotal.io/security !!!
-->
**Affects:** \<Spring Framework version>
@@ -14,4 +14,4 @@ Thanks for taking the time to create an issue. Please read the following:
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.
-->
-->
-29
View File
@@ -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 +0,0 @@
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: gradle/wrapper-validation-action@v1
+10 -22
View File
@@ -1,42 +1,32 @@
# 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/
# 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 +37,4 @@ spring-*/src/main/java/META-INF/MANIFEST.MF
out
test-output
atlassian-ide-plugin.xml
# VS Code
.vscode/
.gradletasknamecache
+22 -33
View File
@@ -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>
-3
View File
@@ -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
+48 -57
View File
@@ -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,72 @@ 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.
member to triage it. Within a day or two, the issue will then be reviewed and the team
may ask for further information if needed. Based on the findings, the issue is either
assigned a fix version or declined.
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 closed and may still be re-opened. Once a fix is
released, the issue can't be reopened. 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
[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 +97,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 +108,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
View File
@@ -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.
+4 -12
View File
@@ -1,6 +1,6 @@
# <img src="framework-docs/src/docs/spring-framework.png" width="80" height="80"> Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-5.3.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](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) Wiki page and the [CONTRIBUTING.md](CONTRIBUTING.md) file.
## Stay in Touch
-11
View File
@@ -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.
+273 -131
View File
@@ -1,177 +1,319 @@
buildscript {
repositories {
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.3.31" 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 != "spring-build-src") && (it.name != "spring-framework-bom") && (it.name != "spring-core-coroutines")
}
aspectjVersion = "1.9.3"
coroutinesVersion = "1.2.1"
freemarkerVersion = "2.3.28"
groovyVersion = "2.5.6"
hsqldbVersion = "2.4.1"
jackson2Version = "2.9.8"
jettyVersion = "9.4.18.v20190429"
junit5Version = "5.4.2"
kotlinVersion = "1.3.31"
log4jVersion = "2.11.2"
nettyVersion = "4.1.36.Final"
reactorVersion = "Dysprosium-M1"
rxjavaVersion = "1.3.8"
rxjavaAdapterVersion = "1.2.1"
rxjava2Version = "2.2.8"
slf4jVersion = "1.7.26" // spring-jcl + consistent 3rd party deps
tiles3Version = "3.0.8"
tomcatVersion = "9.0.19"
undertowVersion = "2.0.20.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"]
}
}
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", '**/*$*'])
reports.junitXml.setDestination(file("$buildDir/test-results"))
}
checkstyle {
toolVersion = "10.4"
configDirectory.set(rootProject.file("src/checkstyle"))
toolVersion = "8.20"
configDir = rootProject.file("src/checkstyle")
}
tasks.named("checkstyleMain").configure {
maxHeapSize = "1g"
}
tasks.named("checkstyleTest").configure {
maxHeapSize = "1g"
repositories {
maven { url "https://repo.spring.io/libs-release" }
maven { url "https://repo.spring.io/milestone" } // Reactor
maven { url "https://oss.jfrog.org/artifactory/libs-snapshot" } // RSocket
mavenLocal()
}
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.13-beta-3") {
exclude group: "org.hamcrest", module: "hamcrest-core"
}
testCompile("org.mockito:mockito-core:2.27.0") {
exclude group: "org.hamcrest", module: "hamcrest-core"
}
testCompile("io.mockk:mockk:1.9.3")
testCompile("org.hamcrest:hamcrest-all:1.3")
testCompile("org.assertj:assertj-core:3.12.2")
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.findAll { (it.name != "spring-build-src") && (it.name != "spring-core-coroutines") } ) { 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.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
}
-75
View File
@@ -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.
-45
View File
@@ -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
View File
@@ -1 +0,0 @@
org.gradle.caching=true
+10
View File
@@ -0,0 +1,10 @@
description = "Exposes gradle buildSrc for IDE support"
apply plugin: "groovy"
dependencies {
compile gradleApi()
compile localGroovy()
}
configurations.archives.artifacts.clear()
@@ -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
*
* 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.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));
});
}
}
@@ -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);
}
}
}
@@ -0,0 +1 @@
implementation-class=org.springframework.build.gradle.TestSourceSetDependenciesPlugin
-57
View File
@@ -1,57 +0,0 @@
== Spring Framework Concourse pipeline
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].
=== Setting up your development environment
If you're part of the Spring Framework project on GitHub, you can get access to CI management features.
First, you need to go to https://ci.spring.io and install the client CLI for your platform (see bottom right of the screen).
You can then login with the instance using:
[source]
----
$ fly -t spring login -n spring-framework -c https://ci.spring.io
----
Once logged in, you should get something like:
[source]
----
$ fly ts
name url team expiry
spring https://ci.spring.io spring-framework Wed, 25 Mar 2020 17:45:26 UTC
----
=== Pipeline configuration and structure
The build pipelines are described in `pipeline.yml` file.
This file is listing Concourse resources, i.e. build inputs and outputs such as container images, artifact repositories, source repositories, notification services, etc.
It also describes jobs (a job is a sequence of inputs, tasks and outputs); jobs are organized by groups.
The `pipeline.yml` definition contains `((parameters))` which are loaded from the `parameters.yml` file or from our https://docs.cloudfoundry.org/credhub/[credhub instance].
You'll find in this folder the following resources:
* `pipeline.yml` the build pipeline
* `parameters.yml` the build parameters used for the pipeline
* `images/` holds the container images definitions used in this pipeline
* `scripts/` holds the build scripts that ship within the CI container images
* `tasks` contains the task definitions used in the main `pipeline.yml`
=== Updating the build pipeline
Updating files on the repository is not enough to update the build pipeline, as changes need to be applied.
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
----
NOTE: This assumes that you have credhub integration configured with the appropriate secrets.
-20
View File
@@ -1,20 +0,0 @@
changelog:
repository: spring-projects/spring-framework
sections:
- title: ":star: New Features"
labels:
- "type: enhancement"
- title: ":lady_beetle: Bug Fixes"
labels:
- "type: bug"
- "type: regression"
- title: ":notebook_with_decorative_cover: Documentation"
labels:
- "type: documentation"
- title: ":hammer: Dependency Upgrades"
sort: "title"
labels:
- "type: dependency-upgrade"
contributors:
exclude:
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll"]
-10
View File
@@ -1,10 +0,0 @@
logging:
level:
io.spring.concourse: DEBUG
spring:
main:
banner-mode: off
sonatype:
exclude:
- 'build-info\.json'
- '.*\.zip'
-21
View File
@@ -1,21 +0,0 @@
== CI Images
These images are used by CI to run the actual builds.
To build the image locally run the following from this directory:
----
$ docker build --no-cache -f <image-folder>/Dockerfile .
----
For example
----
$ docker build --no-cache -f spring-framework-ci-image/Dockerfile .
----
To test run:
----
$ docker run -it --entrypoint /bin/bash <SHA>
----
-12
View File
@@ -1,12 +0,0 @@
FROM ubuntu:focal-20220922
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
ENV PATH $JAVA_HOME/bin:$PATH
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
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"
;;
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"
;;
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
-40
View File
@@ -1,40 +0,0 @@
#!/bin/bash
set -ex
###########################################################
# UTILS
###########################################################
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq fontconfig
ln -fs /usr/share/zoneinfo/UTC /etc/localtime
dpkg-reconfigure --frontend noninteractive tzdata
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
###########################################################
# JAVA
###########################################################
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
###########################################################
# GRADLE ENTERPRISE
###########################################################
cd /
mkdir ~/.gradle
echo 'systemProp.user.name=concourse' > ~/.gradle/gradle.properties
-14
View File
@@ -1,14 +0,0 @@
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"
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
-480
View File
@@ -1,480 +0,0 @@
anchors:
git-repo-resource-source: &git-repo-resource-source
uri: ((github-repo))
username: ((github-username))
password: ((github-ci-release-token))
branch: ((branch))
gradle-enterprise-task-params: &gradle-enterprise-task-params
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
sonatype-task-params: &sonatype-task-params
SONATYPE_USERNAME: ((sonatype-username))
SONATYPE_PASSWORD: ((sonatype-password))
SONATYPE_URL: ((sonatype-url))
SONATYPE_STAGING_PROFILE: ((sonatype-staging-profile))
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
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: &registry-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!>
[$TEXT_FILE_CONTENT]
text_file: git-repo/build/build-scan-uri.txt
silent: true
icon_emoji: ":concourse:"
username: concourse-ci
changelog-task-params: &changelog-task-params
name: generated-changelog/tag
tag: generated-changelog/tag
body: generated-changelog/changelog.md
github-task-params: &github-task-params
GITHUB_USERNAME: ((github-username))
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:
repository: springio/artifactory-resource
tag: 0.0.17
- name: github-release
type: registry-image
source:
repository: concourse/github-release-resource
tag: 1.5.5
- name: github-status-resource
type: registry-image
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:
repository: cfcommunity/slack-notification-resource
tag: latest
resources:
- name: git-repo
type: git
icon: github
source:
<<: *git-repo-resource-source
- name: ci-images-git-repo
type: git
icon: github
source:
uri: ((github-repo))
branch: ((branch))
paths: ["ci/images/*"]
- name: ci-image
type: registry-image
icon: docker
source:
<<: *docker-resource-source
repository: ((docker-hub-organization))/spring-framework-ci
- name: every-morning
type: time
icon: alarm
source:
start: 8:00 AM
stop: 9:00 AM
location: Europe/Vienna
- name: artifactory-repo
type: artifactory-resource
icon: package-variant
source:
uri: ((artifactory-server))
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
source:
repository: ((github-repo-name))
access_token: ((github-ci-status-token))
branch: ((branch))
context: build
- name: repo-status-jdk18-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
- name: slack-alert
type: slack-notification
icon: slack
source:
url: ((slack-webhook-url))
- name: github-pre-release
type: github-release
icon: briefcase-download-outline
source:
owner: spring-projects
repository: spring-framework
access_token: ((github-ci-release-token))
pre_release: true
release: false
- name: github-release
type: github-release
icon: briefcase-download
source:
owner: spring-projects
repository: spring-framework
access_token: ((github-ci-release-token))
pre_release: false
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
- put: ci-image
params:
image: ci-image/image.tar
- name: build
serial: true
public: true
plan:
- get: ci-image
- get: git-repo
trigger: true
- put: repo-status-build
params: { state: "pending", commit: "git-repo" }
- do:
- task: build-project
image: ci-image
file: git-repo/ci/tasks/build-project.yml
privileged: true
timeout: ((task-timeout))
params:
<<: *build-project-task-params
on_failure:
do:
- put: repo-status-build
params: { state: "failure", commit: "git-repo" }
- put: slack-alert
params:
<<: *slack-fail-params
- put: repo-status-build
params: { state: "success", commit: "git-repo" }
- put: artifactory-repo
params: &artifactory-params
signing_key: ((signing-key))
signing_passphrase: ((signing-passphrase))
repo: libs-snapshot-local
folder: distribution-repository
build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}"
build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}"
disable_checksum_uploads: true
threads: 8
artifact_set:
- include:
- "/**/framework-docs-*.zip"
properties:
"zip.name": "spring-framework"
"zip.displayname": "Spring Framework"
"zip.deployed": "false"
- include:
- "/**/framework-docs-*-docs.zip"
properties:
"zip.type": "docs"
- include:
- "/**/framework-docs-*-dist.zip"
properties:
"zip.type": "dist"
- include:
- "/**/framework-docs-*-schema.zip"
properties:
"zip.type": "schema"
get_params:
threads: 8
- name: jdk18-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
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
- name: stage-milestone
serial: true
plan:
- get: ci-image
- get: git-repo
trigger: false
- task: stage
image: ci-image
file: git-repo/ci/tasks/stage-version.yml
params:
RELEASE_TYPE: M
<<: *gradle-enterprise-task-params
- put: artifactory-repo
params:
<<: *artifactory-params
repo: libs-staging-local
- put: git-repo
params:
repository: stage-git-repo
- name: promote-milestone
serial: true
plan:
- get: ci-image
- get: git-repo
trigger: false
- get: artifactory-repo
trigger: false
passed: [stage-milestone]
params:
download_artifacts: false
save_build_info: true
- task: promote
file: git-repo/ci/tasks/promote-version.yml
params:
RELEASE_TYPE: M
<<: *artifactory-task-params
- task: generate-changelog
file: git-repo/ci/tasks/generate-changelog.yml
params:
RELEASE_TYPE: M
<<: *github-task-params
- put: github-pre-release
params:
<<: *changelog-task-params
- name: stage-rc
serial: true
plan:
- get: ci-image
- get: git-repo
trigger: false
- task: stage
image: ci-image
file: git-repo/ci/tasks/stage-version.yml
params:
RELEASE_TYPE: RC
<<: *gradle-enterprise-task-params
- put: artifactory-repo
params:
<<: *artifactory-params
repo: libs-staging-local
- put: git-repo
params:
repository: stage-git-repo
- name: promote-rc
serial: true
plan:
- get: ci-image
- get: git-repo
trigger: false
- get: artifactory-repo
trigger: false
passed: [stage-rc]
params:
download_artifacts: false
save_build_info: true
- task: promote
file: git-repo/ci/tasks/promote-version.yml
params:
RELEASE_TYPE: RC
<<: *artifactory-task-params
- task: generate-changelog
file: git-repo/ci/tasks/generate-changelog.yml
params:
RELEASE_TYPE: RC
<<: *github-task-params
- put: github-pre-release
params:
<<: *changelog-task-params
- name: stage-release
serial: true
plan:
- get: ci-image
- get: git-repo
trigger: false
- task: stage
image: ci-image
file: git-repo/ci/tasks/stage-version.yml
params:
RELEASE_TYPE: RELEASE
<<: *gradle-enterprise-task-params
- put: artifactory-repo
params:
<<: *artifactory-params
repo: libs-staging-local
- put: git-repo
params:
repository: stage-git-repo
- name: promote-release
serial: true
plan:
- get: ci-image
- get: git-repo
trigger: false
- get: artifactory-repo
trigger: false
passed: [stage-release]
params:
download_artifacts: true
save_build_info: true
- task: promote
file: git-repo/ci/tasks/promote-version.yml
params:
RELEASE_TYPE: RELEASE
<<: *artifactory-task-params
<<: *sonatype-task-params
- name: create-github-release
serial: true
plan:
- get: ci-image
- get: git-repo
- get: artifactory-repo
trigger: true
passed: [promote-release]
params:
download_artifacts: false
save_build_info: true
- task: generate-changelog
file: git-repo/ci/tasks/generate-changelog.yml
params:
RELEASE_TYPE: RELEASE
<<: *github-task-params
- put: github-release
params:
<<: *changelog-task-params
groups:
- name: "builds"
jobs: ["build", "jdk18-build", "jdk19-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" ]
-8
View File
@@ -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
-9
View File
@@ -1,9 +0,0 @@
#!/bin/bash
set -e
source $(dirname $0)/common.sh
repository=$(pwd)/distribution-repository
pushd git-repo > /dev/null
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
popd > /dev/null
-9
View File
@@ -1,9 +0,0 @@
#!/bin/bash
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
popd > /dev/null
-2
View File
@@ -1,2 +0,0 @@
source /opt/concourse-java.sh
setup_symlinks
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
set -e
CONFIG_DIR=git-repo/ci/config
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
java -jar /github-changelog-generator.jar \
--spring.config.location=${CONFIG_DIR}/changelog-generator.yml \
${version} generated-changelog/changelog.md
echo ${version} > generated-changelog/version
echo v${version} > generated-changelog/tag
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
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 \
--spring.config.location=${CONFIG_DIR}/release-scripts.yml \
publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; }
java -jar /concourse-release-scripts.jar \
--spring.config.location=${CONFIG_DIR}/release-scripts.yml \
promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; }
echo "Promotion complete"
echo $version > version/version
-50
View File
@@ -1,50 +0,0 @@
#!/bin/bash
set -e
source $(dirname $0)/common.sh
repository=$(pwd)/distribution-repository
pushd git-repo > /dev/null
git fetch --tags --all > /dev/null
popd > /dev/null
git clone git-repo stage-git-repo > /dev/null
pushd stage-git-repo > /dev/null
snapshotVersion=$( awk -F '=' '$1 == "version" { print $2 }' gradle.properties )
if [[ $RELEASE_TYPE = "M" ]]; then
stageVersion=$( get_next_milestone_release $snapshotVersion)
nextVersion=$snapshotVersion
elif [[ $RELEASE_TYPE = "RC" ]]; then
stageVersion=$( get_next_rc_release $snapshotVersion)
nextVersion=$snapshotVersion
elif [[ $RELEASE_TYPE = "RELEASE" ]]; then
stageVersion=$( get_next_release $snapshotVersion)
nextVersion=$( bump_version_number $snapshotVersion)
else
echo "Unknown release type $RELEASE_TYPE" >&2; exit 1;
fi
echo "Staging $stageVersion (next version will be $nextVersion)"
sed -i "s/version=$snapshotVersion/version=$stageVersion/" gradle.properties
git config user.name "Spring Builds" > /dev/null
git config user.email "spring-builds@users.noreply.github.com" > /dev/null
git add gradle.properties > /dev/null
git commit -m"Release v$stageVersion" > /dev/null
git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null
./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
git reset --hard HEAD^ > /dev/null
if [[ $nextVersion != $snapshotVersion ]]; then
echo "Setting next development version (v$nextVersion)"
sed -i "s/version=$snapshotVersion/version=$nextVersion/" gradle.properties
git add gradle.properties > /dev/null
git commit -m"Next development version (v$nextVersion)" > /dev/null
fi;
echo "Staging Complete"
popd > /dev/null
-32
View File
@@ -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
-19
View File
@@ -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
-22
View File
@@ -1,22 +0,0 @@
---
platform: linux
inputs:
- name: git-repo
outputs:
- name: distribution-repository
- 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-project.sh
-24
View File
@@ -1,24 +0,0 @@
---
platform: linux
inputs:
- name: git-repo
outputs:
- name: distribution-repository
- name: git-repo
caches:
- path: gradle
params:
BRANCH:
CI: true
MAIN_TOOLCHAIN:
TEST_TOOLCHAIN:
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/check-project.sh
-20
View File
@@ -1,20 +0,0 @@
---
platform: linux
image_resource:
type: registry-image
source:
repository: springio/github-changelog-generator
tag: '0.0.8'
inputs:
- name: git-repo
- name: artifactory-repo
outputs:
- name: generated-changelog
params:
GITHUB_ORGANIZATION:
GITHUB_REPO:
GITHUB_USERNAME:
GITHUB_TOKEN:
RELEASE_TYPE:
run:
path: git-repo/ci/scripts/generate-changelog.sh
-23
View File
@@ -1,23 +0,0 @@
---
platform: linux
image_resource:
type: registry-image
source:
repository: springio/concourse-release-scripts
tag: '0.3.4'
inputs:
- name: git-repo
- name: artifactory-repo
outputs:
- name: version
params:
RELEASE_TYPE:
ARTIFACTORY_SERVER:
ARTIFACTORY_USERNAME:
ARTIFACTORY_PASSWORD:
SONATYPE_USER:
SONATYPE_PASSWORD:
SONATYPE_URL:
SONATYPE_STAGING_PROFILE:
run:
path: git-repo/ci/scripts/promote-version.sh
-17
View File
@@ -1,17 +0,0 @@
---
platform: linux
inputs:
- name: git-repo
outputs:
- name: stage-git-repo
- name: distribution-repository
params:
RELEASE_TYPE:
CI: true
GRADLE_ENTERPRISE_CACHE_USERNAME:
GRADLE_ENTERPRISE_CACHE_PASSWORD:
GRADLE_ENTERPRISE_URL: https://ge.spring.io
caches:
- path: gradle
run:
path: git-repo/ci/scripts/stage-version.sh
-23
View File
@@ -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
}
}
}
-259
View File
@@ -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,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
@@ -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.
@@ -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)
// ...
}
----
@@ -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.
@@ -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,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,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`>>.
File diff suppressed because it is too large Load Diff
@@ -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`>>.
@@ -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/*");
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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/*");
}
}
}
@@ -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 {
}
}
@@ -1,148 +0,0 @@
plugins {
id 'java-platform'
}
javaPlatform {
allowDependencies()
}
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.14.1"))
api(platform("io.micrometer:micrometer-bom:1.10.0"))
api(platform("io.netty:netty-bom:4.1.85.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2022.0.0"))
api(platform("io.rsocket:rsocket-bom:1.1.3"))
api(platform("org.apache.groovy:groovy-bom:4.0.5"))
api(platform("org.apache.logging.log4j:log4j-bom:2.19.0"))
api(platform("org.eclipse.jetty:jetty-bom:11.0.12"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0"))
api(platform("org.junit:junit-bom:5.9.1"))
api(platform("org.mockito:mockito-bom:4.8.1"))
constraints {
api("com.fasterxml.woodstox:woodstox-core:6.4.0")
api("com.fasterxml:aalto-xml:1.3.1")
api("com.github.ben-manes.caffeine:caffeine:3.1.1")
api("com.github.librepdf:openpdf:1.3.30")
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.9.1")
api("com.google.protobuf:protobuf-java-util:3.21.5")
api("com.googlecode.protobuf-java-format:protobuf-java-format:1.4")
api("com.h2database:h2:2.1.214")
api("com.jayway.jsonpath:json-path:2.7.0")
api("com.rometools:rome:1.18.0")
api("com.squareup.okhttp3:mockwebserver:3.14.9")
api("com.squareup.okhttp3:okhttp:3.14.9")
api("com.sun.activation:jakarta.activation:2.0.1")
api("com.sun.mail:jakarta.mail:2.0.1")
api("com.sun.xml.bind:jaxb-core:3.0.2")
api("com.sun.xml.bind:jaxb-impl:3.0.2")
api("com.sun.xml.bind:jaxb-xjc:3.0.2")
api("com.thoughtworks.qdox:qdox:2.0.2")
api("com.thoughtworks.xstream:xstream:1.4.19")
api("commons-io:commons-io:2.11.0")
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1")
api("info.picocli:picocli:4.6.3")
api("io.micrometer:context-propagation:1.0.0")
api("io.mockk:mockk:1.12.1")
api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3")
api("io.projectreactor.tools:blockhound:1.0.6.RELEASE")
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
api("io.reactivex.rxjava3:rxjava:3.1.5")
api("io.smallrye.reactive:mutiny:1.8.0")
api("io.undertow:undertow-core:2.3.0.Final")
api("io.undertow:undertow-servlet:2.3.0.Final")
api("io.undertow:undertow-websockets-jsr:2.3.0.Final")
api("io.vavr:vavr:0.10.4")
api("jakarta.activation:jakarta.activation-api:2.0.1")
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
api("jakarta.ejb:jakarta.ejb-api:4.0.0")
api("jakarta.el:jakarta.el-api:4.0.0")
api("jakarta.enterprise.concurrent:jakarta.enterprise.concurrent-api:2.0.0")
api("jakarta.faces:jakarta.faces-api:3.0.0")
api("jakarta.inject:jakarta.inject-api:2.0.0")
api("jakarta.inject:jakarta.inject-tck:2.0.1")
api("jakarta.interceptor:jakarta.interceptor-api:2.0.0")
api("jakarta.jms:jakarta.jms-api:3.0.0")
api("jakarta.json.bind:jakarta.json.bind-api:2.0.0")
api("jakarta.json:jakarta.json-api:2.0.1")
api("jakarta.mail:jakarta.mail-api:2.0.1")
api("jakarta.persistence:jakarta.persistence-api:3.0.0")
api("jakarta.resource:jakarta.resource-api:2.0.0")
api("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0")
api("jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.0")
api("jakarta.servlet:jakarta.servlet-api:6.0.0")
api("jakarta.transaction:jakarta.transaction-api:2.0.1")
api("jakarta.validation:jakarta.validation-api:3.0.2")
api("jakarta.websocket:jakarta.websocket-api:2.1.0")
api("jakarta.websocket:jakarta.websocket-client-api:2.1.0")
api("jakarta.xml.bind:jakarta.xml.bind-api:3.0.1")
api("javax.cache:cache-api:1.1.1")
api("javax.money:money-api:1.1")
api("jaxen:jaxen:1.2.0")
api("junit:junit:4.13.2")
api("net.sf.jopt-simple:jopt-simple:5.0.4")
api("net.sourceforge.htmlunit:htmlunit:2.66.0")
api("org.apache-extras.beanshell:bsh:2.0b6")
api("org.apache.activemq:activemq-broker:5.16.2")
api("org.apache.activemq:activemq-kahadb-store:5.16.2")
api("org.apache.activemq:activemq-stomp:5.16.2")
api("org.apache.commons:commons-pool2:2.9.0")
api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1")
api("org.apache.httpcomponents.client5:httpclient5:5.1.3")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.1.3")
api("org.apache.poi:poi-ooxml:5.2.2")
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.1")
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.1")
api("org.apache.tomcat:tomcat-util:10.1.1")
api("org.apache.tomcat:tomcat-websocket:10.1.1")
api("org.aspectj:aspectjrt:1.9.9.1")
api("org.aspectj:aspectjtools:1.9.9.1")
api("org.aspectj:aspectjweaver:1.9.9.1")
api("org.assertj:assertj-core:3.23.1")
api("org.awaitility:awaitility:3.1.6")
api("org.bouncycastle:bcpkix-jdk18on:1.71")
api("org.codehaus.jettison:jettison:1.3.8")
api("org.dom4j:dom4j:2.1.3")
api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.7")
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.3")
api("org.eclipse:yasson:2.0.4")
api("org.ehcache:ehcache:3.4.0")
api("org.ehcache:jcache:1.0.1")
api("org.freemarker:freemarker:2.3.31")
// Substitute for "javax.management:jmxremote_optional:1.0.1_04" which
// is not available on Maven Central
api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea")
api("org.glassfish.tyrus:tyrus-container-servlet:2.0.1")
api("org.glassfish:jakarta.el:4.0.2")
api("org.graalvm.sdk:graal-sdk:22.3.0")
api("org.hamcrest:hamcrest:2.2")
api("org.hibernate:hibernate-core-jakarta:5.6.12.Final")
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.0")
api("org.javamoney:moneta:1.4.2")
api("org.jruby:jruby:9.3.8.0")
api("org.junit.support:testng-engine:1.0.4")
api("org.mozilla:rhino:1.7.11")
api("org.ogce:xpp3:1.1.6")
api("org.python:jython-standalone:2.7.1")
api("org.quartz-scheduler:quartz:2.3.2")
api("org.seleniumhq.selenium:htmlunit-driver:2.66.0")
api("org.seleniumhq.selenium:selenium-java:3.141.59")
api("org.skyscreamer:jsonassert:1.5.0")
api("org.slf4j:slf4j-api:2.0.3")
api("org.testng:testng:7.6.1")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-core:0.48")
api("org.xmlunit:xmlunit-assertj:2.9.0")
api("org.xmlunit:xmlunit-matchers:2.9.0")
api("org.yaml:snakeyaml:1.30")
}
}
+1 -9
View File
@@ -1,9 +1 @@
version=6.0.2
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
org.gradle.parallel=true
kotlinVersion=1.7.21
kotlin.stdlib.default.dependency=false
version=5.2.0.M2
-30
View File
@@ -1,30 +0,0 @@
tasks.findByName("dokkaHtmlPartial")?.configure {
outputDirectory.set(new File(buildDir, "docs/kdoc"))
dokkaSourceSets {
configureEach {
sourceRoots.setFrom(file("src/main/kotlin"))
classpath.from(sourceSets["main"].runtimeClasspath)
externalDocumentationLink {
url.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/"))
}
externalDocumentationLink {
url.set(new URL("https://projectreactor.io/docs/core/release/api/"))
}
externalDocumentationLink {
url.set(new URL("https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/"))
}
externalDocumentationLink {
url.set(new URL("https://kotlin.github.io/kotlinx.coroutines/"))
}
externalDocumentationLink {
url.set(new URL("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/"))
}
externalDocumentationLink {
url.set(new URL("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/"))
}
externalDocumentationLink {
url.set(new URL("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/"))
}
}
}
}
+265
View File
@@ -0,0 +1,265 @@
/*
* Copyright 2002-2018 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.
*/
task api(type: Javadoc) {
group = "Documentation"
description = "Generates aggregated Javadoc API documentation."
title = "${rootProject.description} ${version} API"
dependsOn {
subprojects.collect {
it.tasks.getByName("jar")
}
}
options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED
options.author = true
options.header = rootProject.description
options.use = true
options.overview = "src/docs/api/overview.html"
options.stylesheetFile = file("src/docs/api/stylesheet.css")
options.splitIndex = true
options.links(project.ext.javadocLinks)
options.addStringOption('Xdoclint:none', '-quiet')
source subprojects.collect { project ->
project.sourceSets.main.allJava
}
maxMemory = "1024m"
destinationDir = new File(buildDir, "api")
doFirst {
classpath = files(
// ensure the javadoc process can resolve types compiled from .aj sources
project(":spring-aspects").sourceSets.main.output
)
classpath += files(subprojects.collect { it.sourceSets.main.compileClasspath })
}
}
dokka {
dependsOn {
tasks.getByName("api")
}
doFirst {
classpath = subprojects.collect { project -> project.jar.outputs.files.getFiles() }.flatten()
classpath += files(subprojects.collect { it.sourceSets.main.compileClasspath })
}
moduleName = "spring-framework"
outputFormat = "html"
outputDirectory = "$buildDir/docs/kdoc"
sourceDirs = files(subprojects.collect { project ->
def kotlinDirs = project.sourceSets.main.kotlin.srcDirs.collect()
kotlinDirs -= project.sourceSets.main.java.srcDirs
})
externalDocumentationLink {
url = new URL("https://docs.spring.io/spring-framework/docs/$version/javadoc-api/")
packageListUrl = new File(buildDir, "api/package-list").toURI().toURL()
}
externalDocumentationLink {
url = new URL("https://projectreactor.io/docs/core/release/api/")
}
externalDocumentationLink {
url = new URL("https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/")
}
externalDocumentationLink {
url = new URL("https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/")
}
}
configurations {
docs
}
dependencies {
// for documentation resources
docs "io.spring.docresources:spring-doc-resources:0.1.0.RELEASE@zip"
}
task extractDocResources(type: Sync) {
dependsOn configurations.docs
from "src/docs/asciidoc/"
from {
configurations.docs.collect { zipTree(it) }
}
into "$buildDir/asciidoc/build"
}
asciidoctor {
sourceDir "$buildDir/asciidoc/build"
sources {
include '*.adoc'
}
resources {
from(sourceDir) {
include 'images/*', 'css/**', 'js/**'
}
}
logDocuments = true
backends = ["html5"]
// only ouput PDF documentation for non-SNAPSHOT builds
if(!project.getVersion().toString().contains("BUILD-SNAPSHOT")) {
backends += "pdf"
}
options doctype: 'book', eruby: 'erubis'
attributes 'icons': 'font',
'idprefix': '',
'idseparator': '-',
docinfo: 'shared',
revnumber: project.version,
sectanchors: '',
sectnums: '',
'source-highlighter=highlight.js',
'highlightjsdir=js/highlight',
'highlightjs-theme=atom-one-dark-reasonable',
stylesdir: "css/",
stylesheet: 'spring.css',
'spring-version': project.version
}
asciidoctor.dependsOn extractDocResources
task docsZip(type: Zip, dependsOn: ['api', 'asciidoctor', 'dokka']) {
group = "Distribution"
baseName = "spring-framework"
classifier = "docs"
description = "Builds -${classifier} archive containing api and reference " +
"for deployment at https://docs.spring.io/spring-framework/docs."
from("src/dist") {
include "changelog.txt"
}
from (api) {
into "javadoc-api"
}
from ("$asciidoctor.outputDir/html5") {
into "spring-framework-reference"
}
from ("$asciidoctor.outputDir/pdf") {
into "spring-framework-reference/pdf"
}
from (dokka) {
into "kdoc-api"
}
}
task schemaZip(type: Zip) {
group = "Distribution"
baseName = "spring-framework"
classifier = "schema"
description = "Builds -${classifier} archive containing all " +
"XSDs for deployment at https://springframework.org/schema."
duplicatesStrategy 'exclude'
moduleProjects.each { subproject ->
def Properties schemas = new Properties();
subproject.sourceSets.main.resources.find {
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 = subproject.sourceSets.main.resources.find {
it.path.endsWith(schemas.get(key))
}
assert xsdFile != null
into (shortName) {
from xsdFile.path
}
}
}
}
task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) {
group = "Distribution"
baseName = "spring-framework"
classifier = "dist"
description = "Builds -${classifier} archive, containing all jars and docs, " +
"suitable for community download page."
ext.baseDir = "${baseName}-${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.archivePath)) {
into "${baseDir}/docs"
}
from(zipTree(schemaZip.archivePath)) {
into "${baseDir}/schema"
}
moduleProjects.each { subproject ->
into ("${baseDir}/libs") {
from subproject.jar
if (subproject.tasks.findByPath("sourcesJar")) {
from subproject.sourcesJar
}
if (subproject.tasks.findByPath("javadocJar")) {
from subproject.javadocJar
}
}
}
}
distZip.mustRunAfter subprojects.test
// Create a distribution that contains all dependencies (required and optional).
// Not published by default; only for use when building from source.
task depsZip(type: Zip, dependsOn: distZip) { zipTask ->
group = "Distribution"
baseName = "spring-framework"
classifier = "dist-with-deps"
description = "Builds -${classifier} archive, containing everything " +
"in the -${distZip.classifier} archive plus all runtime dependencies."
from zipTree(distZip.archivePath)
gradle.taskGraph.whenReady { taskGraph ->
if (taskGraph.hasTask(":${zipTask.name}")) {
def projectNames = rootProject.subprojects*.name
def artifacts = new HashSet()
subprojects.each { subproject ->
(subproject.configurations.runtime.resolvedConfiguration.resolvedArtifacts +
subproject.configurations.optional.resolvedConfiguration.resolvedArtifacts).each { artifact ->
def dependency = artifact.moduleVersion.id
if (!projectNames.contains(dependency.name)) {
artifacts << artifact.file
}
}
}
zipTask.from(artifacts) {
into "${distZip.baseDir}/deps"
}
}
}
}
+71 -39
View File
@@ -1,11 +1,13 @@
import org.gradle.plugins.ide.eclipse.model.ProjectDependency
import org.gradle.plugins.ide.eclipse.model.SourceFolder
apply plugin: 'eclipse'
apply plugin: "propdeps-eclipse"
apply plugin: "propdeps-idea"
eclipse.jdt {
sourceCompatibility = 17
targetCompatibility = 17
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
// Replace classpath entries with project dependencies (GRADLE-1116)
@@ -18,73 +20,103 @@ eclipse.classpath.file.whenMerged { classpath ->
if (matcher) {
def projectName = matcher[0][1]
def path = "/${projectName}"
if (!classpath.entries.find { e -> e instanceof ProjectDependency && e.path == path }) {
def recursiveDependency = entry.path.matches('.+/' + projectName + '/build/([^/]+/)+(?:main|test)')
// Avoid recursive dependency on current project.
if (!recursiveDependency) {
classpath.entries.add(new ProjectDependency(path))
}
if(!classpath.entries.find { e -> e instanceof ProjectDependency && e.path == path }) {
def dependency = new ProjectDependency(path)
dependency.exported = true
classpath.entries.add(dependency)
}
classpath.entries.remove(entry)
}
}
// Remove any remaining direct depencencies on JARs in the build/libs folder
// except the repack JARs.
classpath.entries.removeAll { entry -> (entry.path =~ /(?!.*?repack.*\.jar).*?\/([^\/]+)\/build\/libs\/[^\/]+\.jar/) }
}
// Use separate main/test outputs (prevents WTP from packaging test classes)
eclipse.classpath.defaultOutputDir = file(project.name + '/bin/eclipse')
eclipse.classpath.defaultOutputDir = file(project.name+"/bin/eclipse")
eclipse.classpath.file.beforeMerged { classpath ->
classpath.entries.findAll{ it instanceof SourceFolder }.each {
if (it.output.startsWith('bin/')) {
if(it.output.startsWith("bin/")) {
it.output = null
}
}
}
eclipse.classpath.file.whenMerged {
entries.findAll{ it instanceof SourceFolder }.each {
it.output = 'bin/' + it.path.split('/')[1]
eclipse.classpath.file.whenMerged { classpath ->
classpath.entries.findAll{ it instanceof SourceFolder }.each {
it.output = "bin/" + it.path.split("/")[1]
}
}
// Ensure project dependencies come after 3rd-party libs (SPR-11836)
// https://jira.spring.io/browse/SPR-11836
eclipse.classpath.file.whenMerged {
entries.findAll { it instanceof ProjectDependency }.each {
eclipse.classpath.file.whenMerged { classpath ->
classpath.entries.findAll { it instanceof ProjectDependency }.each {
// delete from original position
entries.remove(it)
classpath.entries.remove(it)
// append to end of classpath
entries.add(it)
classpath.entries.add(it)
}
}
// Ensure that JMH sources and resources are treated as test classpath entries
// so that they can see test fixtures.
// https://github.com/melix/jmh-gradle-plugin/issues/157
eclipse.classpath.file.whenMerged {
entries.findAll { it.path =~ /src\/jmh\/(java|kotlin|resources)/ }.each {
it.entryAttributes['test'] = 'true'
}
}
// Allow projects to be used as WTP modules
eclipse.project.natures "org.eclipse.wst.common.project.facet.core.nature"
// Include project specific settings
task eclipseSettings(type: Copy) {
from rootProject.files(
'src/eclipse/org.eclipse.core.resources.prefs',
'src/eclipse/org.eclipse.jdt.core.prefs',
'src/eclipse/org.eclipse.jdt.ui.prefs')
"src/eclipse/org.eclipse.jdt.ui.prefs",
"src/eclipse/org.eclipse.wst.common.project.facet.core.xml")
into project.file('.settings/')
outputs.upToDateWhen { false }
}
task cleanEclipseSettings(type: Delete) {
delete project.file('.settings/org.eclipse.core.resources.prefs')
delete project.file('.settings/org.eclipse.jdt.core.prefs')
delete project.file('.settings/org.eclipse.jdt.ui.prefs')
task eclipseWstComponent(type: Copy) {
from rootProject.files(
"src/eclipse/org.eclipse.wst.common.component")
into project.file('.settings/')
expand(deployname: project.name)
outputs.upToDateWhen { false }
}
tasks['eclipse'].dependsOn(eclipseSettings)
tasks['eclipseJdt'].dependsOn(eclipseSettings)
tasks['cleanEclipse'].dependsOn(cleanEclipseSettings)
task eclipseJdtPrepare(type: Copy) {
from rootProject.file("src/eclipse/org.eclipse.jdt.core.prefs")
into project.file(".settings/")
outputs.upToDateWhen { false }
}
task cleanEclipseJdtUi(type: Delete) {
delete project.file(".settings/org.eclipse.jdt.core.prefs")
delete project.file(".settings/org.eclipse.jdt.ui.prefs")
delete project.file(".settings/org.eclipse.wst.common.component")
delete project.file(".settings/org.eclipse.wst.common.project.facet.core.xml")
}
task eclipseBuildship(type: Copy) {
from rootProject.files(
"src/eclipse/org.eclipse.jdt.ui.prefs",
"src/eclipse/org.eclipse.jdt.core.prefs")
into project.file('.settings/')
outputs.upToDateWhen { false }
}
tasks["eclipseJdt"].dependsOn(eclipseJdtPrepare)
tasks["cleanEclipse"].dependsOn(cleanEclipseJdtUi)
tasks["eclipse"].dependsOn(eclipseSettings, eclipseWstComponent)
// Filter 'build' folder
eclipse.project.file.withXml {
def node = it.asNode()
def filteredResources = node.get("filteredResources")
if(filteredResources) {
node.remove(filteredResources)
}
def filterNode = node.appendNode("filteredResources").appendNode("filter")
filterNode.appendNode("id", "1359048889071")
filterNode.appendNode("name", "")
filterNode.appendNode("type", "30")
def matcherNode = filterNode.appendNode("matcher")
matcherNode.appendNode("id", "org.eclipse.ui.ide.multiFilter")
matcherNode.appendNode("arguments", "1.0-projectRelativePath-matches-false-false-build")
}
+76
View File
@@ -0,0 +1,76 @@
/**
* Generate a JDiff report between the current version and an older version.
*
* Usage:
* gradle jdiff -D OLD_VERSION=3.1.3.RELEASE -D OLD_VERSION_ROOT=/path/to/3.1.3.RELEASE
*
* View generated report at:
* build/reports/jdiff/changes.html
*
* @param OLD_VERSION required
* @param OLD_VERSION_ROOT required, typically pointing to a separate git clone dir
*/
task jdiff {
description = "Generates a JDiff report"
group = "Documentation"
def jdiffHome = "${rootProject.rootDir}/gradle/jdiff"
ant.taskdef(
name: "jdiff",
classname: "jdiff.JDiffAntTask",
classpath: "${jdiffHome}/antjdiff.jar")
def currentVersion = rootProject.version
def currentVersionRoot = rootProject.rootDir
def oldVersion = System.getProperty("OLD_VERSION")
def oldVersionRoot = System.getProperty("OLD_VERSION_ROOT")
def outputDir = "${rootProject.buildDir}/reports/jdiff/${oldVersion}_to_${currentVersion}"
doLast {
if (oldVersion == null)
throw new IllegalArgumentException(
"Set OLD_VERSION property to indicate older of the two versions being compared")
if (oldVersionRoot == null)
throw new IllegalArgumentException(
"Set OLD_VERSION_ROOT property to indicate the root directory for ${oldVersion}")
oldVersionRoot = new File(oldVersionRoot)
ant.property(name: "JDIFF_HOME", value: jdiffHome)
ant.mkdir(dir: outputDir)
ant.jdiff(
destdir: outputDir,
source: "1.8",
verbose: "off",
stats: "on",
docchanges: "off") {
old(name: "Spring Framework ${oldVersion}") {
oldVersionRoot.eachDirMatch( {
def candidate = new File(it)
candidate.name.matches("org.springframework.*") ||
candidate.name.matches("spring-.*") }) { match ->
match.eachDirRecurse { subdir ->
if (subdir.path ==~ '.*/src/main/java$') {
dirset(dir: subdir.path, includes: "org/**")
}
}
}
}
"new"(name: "Spring Framework ${currentVersion}") {
currentVersionRoot.eachDirMatch( {
def candidate = new File(it)
candidate.name.matches("org.springframework.*") ||
candidate.name.matches("spring-.*") }) { match ->
match.eachDirRecurse { subdir ->
if (subdir.path ==~ '.*/src/main/java$') {
dirset(dir: subdir.path, includes: "org/**")
}
}
}
}
}
}
}
+506
View File
@@ -0,0 +1,506 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
+9
View File
@@ -0,0 +1,9 @@
/**
* This class is used only as a "null" argument for Javadoc when comparing
* two API files. Javadoc has to have a package, .java or .class file as an
* argument, even though JDiff doesn't use it.
*/
public class Null {
public Null() {
}
}
+6
View File
@@ -0,0 +1,6 @@
This distribution of JDiff 1.1.1 is included in the Spring Framework build
because JDiff has a hard requirement on a JDIFF_HOME directory containing
jdiff.jar and xerces.jar as well as other presentation resources.
The actual generation of JDiff reports is driven by the `jdiff` task declared
in build.gradle in the project root.
+59
View File
@@ -0,0 +1,59 @@
JDiff Doclet
------------
Matthew Doar
mdoar@pobox.com
The JDiff doclet is used to generate a report describing the
difference between two public Java APIs.
The file jdiff.html contains the reference page for JDiff. The latest
version of JDiff can be downloaded at:
https://sourceforge.net/projects/javadiff
To use the Ant task on your own project, see example.xml. More examples
of using JDiff to compare the public APIs of J2SE1.3 and J2SE1.4 can
be seen at http://javadiff.sourceforge.net/
For an example with the source distribution, run "ant" and
look at the HTML output in ./build/reports/example/changes.html
The page at ./build/reports/example/changes/com.acme.sp.SPImpl.html
shows what a typical page of changes looks like.
System Requirements
-------------------
JDiff has been tested with all releases of Java since J2SE1.2 but
releases of JDiff after 1.10.0 focus on JDK1.5.
License
-------
JDiff is licensed under the Lesser GNU General Public License (LGPL).
See the file LICENSE.txt.
Acknowledgements
----------------
JDiff uses Stuart D. Gathman's Java translation of Gene Myers' O(ND)
difference algorithm.
JDiff uses Xerces 1.4.2 from https://www.apache.org.
JDiff also includes a script to use the classdoc application from
http://classdoc.sourceforge.net or http://www.jensgulden.de, by Jens
Gulden, (mail@jensgulden.de), to call a doclet such as jdiff on a .jar
file rather than on source.
Many thanks to the reviewers at Sun and Vitria who gave feedback on early
versions of JDiff output, and also to the distillers of Laphroaig, and to
Arturo Fuente for his consistently fine cigars which helped inspire
much of this work.
Footnote:
If you are looking for a generalized diff tool for XML, try diffmk from
https://wwws.sun.com/software/xml/developers/diffmk/
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Some files were not shown because too many files have changed in this diff Show More